Add amber mcp serve: stateless Streamable HTTP MCP server - #32
Draft
crimson-knight wants to merge 1 commit into
Draft
Add amber mcp serve: stateless Streamable HTTP MCP server#32crimson-knight wants to merge 1 commit into
amber mcp serve: stateless Streamable HTTP MCP server#32crimson-knight wants to merge 1 commit into
Conversation
Exposes the CLI to agents over the Model Context Protocol on a single endpoint, POST /mcp, with no session state in any code path. Dual-era on one endpoint: a client that opens with `initialize` gets the 2025-11-25 handshake Claude Desktop speaks today; a client that sends header-versioned 2026-07-28 requests is served statelessly, with the mirrored-header validation, per-request `_meta` and `server/discover` that revision requires. Neither path mints or echoes an Mcp-Session-Id. Eight tools, each taking an explicit absolute path rather than reading the process working directory: amber_version, project_info, list_routes, list_generators, search_docs, read_doc, and the two mutating ones, create_new_app and generate_component. The mutating tools re-invoke the `amber` binary as a child process. They cannot be called in-process: NewCommand and GenerateCommand report every validation failure with `exit(1)`, which inside a long-running server would terminate it and drop every other client. A child process also gives each call its own working directory, so concurrent scaffolding cannot interfere. `routes.cr` gains an explicit routes-file argument on `parse_routes` so the list_routes tool can reuse the existing parser without `Dir.cd`, which is process-global and would race between in-flight requests. Security model for v1 is localhost-only: there is no authentication, so a non-loopback bind is refused unless --allow-remote is passed, and the Origin header is validated to close the DNS-rebinding path. Documentation for search_docs/read_doc is embedded at compile time, since `amber` ships as a single binary with no docs directory beside it.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Adds
amber mcp serve, a Model Context Protocol server that exposes the CLI to agents (Claude Desktop first) over Streamable HTTP on a single endpoint,POST /mcp.Design
Dual-era on one endpoint, stateless in both paths. A client that opens with
initializegets the 2025-11-25 handshake Claude Desktop speaks today. A client that sends header-versioned2026-07-28requests is served statelessly, with the mirrored-header validation, per-request_metaandserver/discoverthat revision requires. Neither path mints or echoes anMcp-Session-Id;GET/DELETEon the endpoint return405, andLast-Event-IDis ignored.Era is decided per request from the version the client declares (body
_metafirst, then theMCP-Protocol-Versionheader — the body is the source of truth and a header that disagrees is rejected, not preferred). A client that declares nothing is treated as legacy rather than rejected: the mirrored-header rules only exist from 2026-07-28, so assuming the modern era for a silent client would fail it with an error it cannot act on.Dispatch is on the
methodstring before any params are decoded. MCP request types are almost all all-optional objects, so decoding into a union picks whichever member parses first and silently runs the wrong handler.The mutating tools re-invoke the
amberbinary as a child process. They cannot be called in-process:NewCommandandGenerateCommandreport every validation failure withexit(1)(11 call sites between them), which inside a long-running server would terminate it and drop every other client. A child process also gives each call its own working directory, so concurrent scaffolding of different applications cannot interfere —Dir.cdis process-global and would race between in-flight fibers.Every application-scoped tool takes an explicit absolute
path. Nothing reads the process working directory, which is wherever the user happened to launch the server.Tools
amber_versionproject_infolist_routesRoutesCommand's parser; static, never boots the applist_generatorssearch_docs/read_doccreate_new_app--no-depsby defaultgenerate_componentAll eight carry
readOnlyHint/destructiveHintannotations so a host can auto-approve introspection and gate the writers. Results are text content blocks with the JSON payload fenced in the text and repeated instructuredContent. Tool failures come back asisError: trueresults, not protocol errors, so the model can see them and self-correct.Security model (v1)
There is no authentication. The model is localhost-only:
127.0.0.1:5757by default.0.0.0.0,::,*) unless--allow-remoteis passed, and warns that remote exposure is unsupported when it is.Originheader and returns403for a non-loopback origin, closing the DNS-rebinding path the specification calls out.Documentation is embedded at compile time
amberships as a single binary through Homebrew, so a docs directory resolved at runtime would be absent on every installed copy.DocumentIndexreads the Markdown into the binary withread_file.Dependency
Branch pin. This is the branch that adds 2026-07-28 / 2025-11-25 support,
server/discover, MRTR types and cache hints. Re-pin to a version tag once mcprotocol PR #1 merges and tags. The shard is bindings-only; the HTTP transport in this PR is ours.One change outside
src/amber_cli/mcp/RoutesCommand#parse_routesgains an explicit routes-file argument (defaulting to the original relative path, soamber routesis unchanged) solist_routescan reuse the parser withoutDir.cd. Deliberately avoidshelpers/process_runner.crto stay clear of #24.Validation
crystal spec63 new examples: 24 HTTP-level integration, 31 tool, 8 command.
Note for reviewers: on a fresh checkout the suite reports 6 failures in
spec/amber_lsp/integration/untilbin/amber-lspis built — those specs assert against the binary and are unrelated to this branch. With both binaries built the suite is fully green.crystal tool format --check src specamebaon the new filesManual E2E
Built binary,
amber mcp servein the background, driven with curl.Startup
Flow A — classic 2025-11-25 (what Claude Desktop speaks)
resultTypeis correctly absent on the legacy result, and noMcp-Session-Idheader is returned.Flow B — stateless 2026-07-28, no handshake at all
tools/callwith no priorinitialize, self-describing via_meta+Mcp-Method/Mcp-Name:Full mutating chain, over the stateless flow
Protocol error paths
SSE framing
Conforming clients send
Accept: application/json, text/event-streamon every request, so "mentionstext/event-stream" cannot be the trigger — that would force SSE always. SSE is used only when the client accepts the event stream and not JSON:Remote bind refusal
Graceful shutdown
Follow-ups (out of scope here)
--allow-remoteactually supportable.watch,execanddatabase.search_docscurrently indexes the CLI's own docs. Once the FSDD docs generator lands onmainit should index that corpus instead — it is not onmaintoday.