-
Notifications
You must be signed in to change notification settings - Fork 12.5k
feat(mcp): add experimental version-only stdio server #4822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mnriem
merged 6 commits into
github:main
from
mnriem:mnriem-experimental-version-mcp-server
Oct 2, 2026
+1,604
−108
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
759177a
feat(mcp): add experimental version server
mnriem 1755d88
fix(mcp): declare schema dependency
mnriem 1f8cde9
fix(mcp): validate child payloads strictly
mnriem 5ae9c34
fix(mcp): isolate worker module lookup
mnriem 56ff5b3
fix(mcp): preserve structured tool errors
mnriem d13c035
test(mcp): bound stdio integration reads
mnriem File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| }, | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.