Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
795 changes: 689 additions & 106 deletions .github/security-audit-requirements.txt

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/reference/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,14 @@ A quick version check is also available via:
specify --version
specify -V
```

## Experimental MCP Server

```bash
specify mcp
```

Starts the experimental stdio-only MCP server. The initial server exposes only
the stable `version` JSON command through generic list, describe, and run tools.
See the [MCP Server reference](mcp.md) for the tool names, result contract, and
current limitations.
50 changes: 50 additions & 0 deletions docs/reference/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# MCP Server

> [!WARNING]
> `specify mcp` is experimental. Its command inventory and tool contracts may
> change before the MCP surface is declared stable.
```bash
specify mcp
```

Starts a Model Context Protocol server over **stdio only**. The command does
not provide HTTP, SSE, daemon/service management, or transport-selection
options. Configure an MCP host to launch `specify mcp` as a local subprocess.

Because stdout carries MCP protocol frames, the server does not print the Spec
Kit banner, Rich output, startup messages, warnings, or logs there.

## Tools

The server exposes three generic tools:

| Tool | Purpose |
| --- | --- |
| `specify_list_commands` | List CLI commands currently supported through MCP |
| `specify_describe_command` | Describe one supported dotted CLI command |
| `specify_run_command` | Run one supported command through the real Specify CLI |

This first experimental release supports only the dotted command `version`.
Every other command name is rejected with an `unavailable_command` tool error.
The server does not expose project discovery, artifacts, mutations,
installation or update operations, workflows, confirmations, or access tiers.

## Version result and errors

`specify_run_command` invokes the canonical command below in an isolated child
process using the same Python environment as the running CLI:

```bash
specify version --json
```

On success, the MCP tool returns that command's direct JSON object with
`cli_version`, `runtime`, `system`, and `features`. It does not wrap the result
in universal `schema_version`, `ok`, or `result` fields.

On CLI failure, the adapter consumes the structured JSON error from stderr and
returns an MCP tool error containing `error.code`, `error.message`, and
`error.details`. Empty, malformed, mixed, or non-JSON child-process output is
reported as a sanitized `adapter_internal_error`; the server never substitutes
a success-shaped fallback or exposes a traceback.
8 changes: 8 additions & 0 deletions docs/reference/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ The foundational commands for creating and managing Spec Kit projects. Initializ

[Core Commands reference →](core.md)

## MCP Server

The experimental `specify mcp` command exposes stable CLI JSON commands to MCP
clients through a local stdio server. The initial surface is deliberately
version-only.

[MCP Server reference →](mcp.md)

## Integrations

Integrations connect Spec Kit to your AI coding agent. Each integration sets up the appropriate command files and directory structures for a specific agent. Only one integration is active per project at a time, and you can switch between them at any point.
Expand Down
2 changes: 2 additions & 0 deletions docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
href: reference/overview.md
- name: Core Commands
href: reference/core.md
- name: MCP Server
href: reference/mcp.md
- name: Integrations
href: reference/integrations.md
- name: Extensions
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies = [
"packaging>=23.0",
"pathspec>=0.12.0",
"json5>=0.13.0",
"mcp>=2.2.0,<3.0.0",
Comment thread
mnriem marked this conversation as resolved.
"pydantic>=2.13.0,<3.0.0",
]

[project.scripts]
Expand Down
4 changes: 4 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# "json5",
# "pyyaml",
# "packaging",
# "mcp>=2.2.0,<3.0.0",
# "pydantic>=2.13.0,<3.0.0",
# ]
# ///
"""
Expand Down Expand Up @@ -386,11 +388,13 @@ def _print_cli_warning(

from . import command_check as _command_check # noqa: E402
from . import command_init as _command_init # noqa: E402
from . import command_mcp as _command_mcp # noqa: E402
from . import command_version as _command_version # noqa: E402

_command_init.register(app)
_command_check.register(app)
_command_version.register(app)
_command_mcp.register(app)

# Preserve root imports for handlers that were previously defined here.
check = _command_check.check
Expand Down
17 changes: 17 additions & 0 deletions src/specify_cli/command_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""CLI adapter for the experimental ``specify mcp`` stdio server."""

from __future__ import annotations

import typer


def mcp() -> None:
"""Run the experimental version-only MCP server over stdio."""
from .mcp_server import run_stdio_server

run_stdio_server()


def register(app: typer.Typer) -> None:
"""Register ``specify mcp`` on the root application."""
app.command()(mcp)
5 changes: 5 additions & 0 deletions src/specify_cli/mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Experimental stdio MCP server for stable Specify CLI JSON commands."""

from .server import create_server, run_stdio_server

__all__ = ["create_server", "run_stdio_server"]
6 changes: 6 additions & 0 deletions src/specify_cli/mcp_server/_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Private child-process entry point for invoking the real Specify CLI."""

from .. import main

if __name__ == "__main__":
main()
134 changes: 134 additions & 0 deletions src/specify_cli/mcp_server/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Explicit command inventory for the experimental MCP server."""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict


class CommandArgument(BaseModel):
"""One named argument accepted by an MCP-exposed CLI command."""

model_config = ConfigDict(extra="forbid")

name: str
type: str
required: bool
description: str


class CommandDescription(BaseModel):
"""Stable description of one MCP-exposed CLI command."""

model_config = ConfigDict(extra="forbid")

command: Literal["version"]
description: str
arguments: list[CommandArgument]
read_only: Literal[True]
json_output: Literal[True]


class CommandList(BaseModel):
"""List of commands currently exposed through MCP."""

model_config = ConfigDict(extra="forbid")

commands: list[CommandDescription]


class RuntimeInfo(BaseModel):
"""Runtime portion of the stable version JSON result."""

model_config = ConfigDict(extra="forbid")

python: str
openssl: str | None


class SystemInfo(BaseModel):
"""System portion of the stable version JSON result."""

model_config = ConfigDict(extra="forbid")

platform: str
architecture: str
os_version: str


class VersionResult(BaseModel):
"""Direct success payload returned by ``specify version --json``."""

model_config = ConfigDict(extra="forbid")

cli_version: str
runtime: RuntimeInfo
system: SystemInfo
features: dict[str, bool]


class CommandAdapterError(Exception):
"""Sanitized command error suitable for conversion to an MCP tool error."""

def __init__(
self,
code: str,
message: str,
details: dict[str, object] | None = None,
) -> None:
super().__init__(message)
self.code = code
self.message = message
self.details = details or {}

def payload(self) -> dict[str, object]:
"""Return the stable structured tool-error payload."""
return {
"error": {
"code": self.code,
"message": self.message,
"details": self.details,
}
}


_VERSION_DESCRIPTION = CommandDescription(
command="version",
description=(
"Return the installed Spec Kit CLI version, runtime, system, and "
"feature capabilities."
),
arguments=[],
read_only=True,
json_output=True,
)
_SUPPORTED_COMMANDS = {"version": _VERSION_DESCRIPTION}


def list_commands() -> CommandList:
"""Return the deliberately minimal supported command inventory."""
return CommandList(commands=list(_SUPPORTED_COMMANDS.values()))


def describe_command(command: str) -> CommandDescription:
"""Return one supported command description or an unavailable error."""
description = _SUPPORTED_COMMANDS.get(command)
if description is None:
raise unavailable_command(command)
return description


def unavailable_command(command: str) -> CommandAdapterError:
"""Build a stable error for a command outside the explicit inventory."""
return CommandAdapterError(
code="unavailable_command",
message=(
f"Command '{command}' is not available through the experimental "
"Spec Kit MCP server."
),
details={
"command": command,
"available_commands": list(_SUPPORTED_COMMANDS),
},
)
Loading
Loading