feat(mcp): tool-extension + adapter-injection seam for the HTTP server - #100
Merged
Merged
Conversation
Add a supported composition seam so a downstream consumer can extend the MCP
server without forking or monkeypatching core:
- tools.register(name, handler, description, inputSchema) — adds a tool to the
shared TOOLS registry with a duplicate-name guard, so a consumer can't
silently shadow a core tool like execute_sql.
- mcp_http.create_app(extra_tools={}, adapters=None) — a composition factory
that merges extra tools over a COPY of TOOLS (never mutating the module
global) and injects the four ports.py adapters, defaulting to the OSS
adapters when None.
- ports.Adapters — a frozen container bundling the four port adapters so they
pass as one argument.
Additive and no-op by default: create_app() with no args behaves identically
to the previous build_app() (kept as a thin wrapper, so `python -m mcp_http`
and main() are unchanged), execute_sql's inputSchema stays byte-identical, and
the stdio entrypoint is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vishalkalbi27
requested review from
ashwin-agami and
sandeep-agami
as code owners
July 10, 2026 04:47
|
All contributors have signed the CLA. Thank you! |
Collaborator
Author
|
I have read the CLA Document and I hereby sign the CLA |
Collaborator
Author
|
recheck |
There was a problem hiding this comment.
Pull request overview
Adds an extension/composition seam for the MCP HTTP server so downstream consumers can add tools and (intended) swap port adapters without forking core, while keeping the default deployment behavior unchanged.
Changes:
- Adds
tools.register(...)to extend the sharedTOOLSregistry with a duplicate-name guard. - Introduces
ports.Adapters(frozen dataclass) to bundle the four port adapters. - Refactors
mcp_httpto providecreate_app(extra_tools, adapters)(keepingbuild_app()as a wrapper) and allows building the MCP server from an injected registry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tests/test_tool_extension_seam.py | New tests covering the extension seam, registry non-mutation, adapter defaults/injection, and auth challenge parity. |
| packages/agami-core/src/tools.py | Adds register() helper to safely extend the tool registry. |
| packages/agami-core/src/ports.py | Adds Adapters dataclass to bundle the four port adapters for composition-root injection. |
| packages/agami-core/src/mcp_http.py | Adds default_adapters(), allows build_server(registry=...), and introduces create_app(extra_tools, adapters) while retaining build_app(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+345
to
+346
| # Merge the consumer's extra tools over a COPY of TOOLS — the module global is never mutated. | ||
| registry = {**TOOLS, **extra_tools} |
Comment on lines
+101
to
+104
| def default_adapters() -> Adapters: | ||
| """The OSS default adapters bundled for the composition root (env-driven auth + org, exactly as | ||
| today). `create_app(adapters=None)` uses these — so a plain deploy is unchanged; a consumer | ||
| passes its own `Adapters(...)` to swap org-resolution/auth/sink/governance without forking core.""" |
Comment on lines
+1185
to
+1198
| def register( | ||
| name: str, | ||
| handler: Callable[[dict], str], | ||
| description: str, | ||
| inputSchema: dict[str, Any], | ||
| ) -> None: | ||
| """Add a tool to the shared TOOLS registry — the supported consumer extension point. | ||
|
|
||
| Raises on a duplicate name so a consumer can't silently shadow a core tool (e.g. execute_sql). | ||
| Note create_app merges a consumer's extra tools over a *copy* of TOOLS; register() mutates the | ||
| module global directly (the stdio path uses it), so its dup-guard is the safety net either way.""" | ||
| if name in TOOLS: | ||
| raise ValueError(f"tool {name!r} is already registered") | ||
| TOOLS[name] = {"handler": handler, "description": description, "inputSchema": inputSchema} |
| COPY of the shared TOOLS (never mutating the global) and injects the four port `adapters` (OSS | ||
| defaults when None). `create_app()` with no args == the historical `build_app()` behavior. | ||
|
|
||
| `extra_tools={}` is read-only here (merged, never mutated), so the shared default is safe.""" |
…table create_app default
- create_app: `extra_tools` default `{}` -> `None` (merges `extra_tools or {}`), avoiding a
mutable default and making an explicit `None` safe.
- Docstrings (ports.Adapters, mcp_http.default_adapters/create_app): clarify that create_app
wires auth_provider + org_resolver into the request path, while activity_sink + governance are
carried on the Adapters container and not yet referenced by a core call site.
- create_app docstring: note that reusing a tool name in extra_tools intentionally overrides at
the composition root (tools.register is the guarded path that refuses a duplicate).
- tools.register: tighten handler type to Callable[[dict[str, Any]], str].
- test: create_app(extra_tools=None) parity with the no-arg call.
No behavior change on the default path; execute_sql's inputSchema stays byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines
+349
to
353
| # Merge the consumer's extra tools over a COPY of TOOLS — the module global is never mutated. | ||
| registry = {**TOOLS, **(extra_tools or {})} | ||
| session_manager = StreamableHTTPSessionManager( | ||
| app=build_server(), json_response=True, stateless=True | ||
| app=build_server(registry), json_response=True, stateless=True | ||
| ) |
| # 307-redirect it (claude.ai posts `{base}/mcp` and won't follow the redirect). See _NormalizeMcpSlash. | ||
| Middleware(_NormalizeMcpSlash), | ||
| Middleware(_AuthMiddleware, resolver=_build_org_resolver(), auth=auth_provider), | ||
| Middleware(_AuthMiddleware, resolver=adapters.org_resolver, auth=auth_provider), |
… extra tools - _AuthMiddleware.resolver is now annotated `OrgResolver` (the protocol) instead of the concrete SingleTenantOrgResolver — create_app injects any resolver via adapters, so the concrete hint was too narrow and misled consumers / type-checkers. - create_app validates each extra_tools entry (dict with handler/description/inputSchema, callable handler) up front, so a malformed entry fails fast at construction with a clear error instead of later as a KeyError/500 inside tools/list or tools/call. - test: create_app rejects a malformed extra tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Adds a supported composition seam so a downstream consumer can extend the MCP server without forking or monkeypatching core. Additive and no-op by default — an existing deploy behaves exactly as before.
Changes
tools.register(name, handler, description, inputSchema)— adds a tool to the sharedTOOLSregistry, with a duplicate-name guard so a consumer can't silently shadow a core tool likeexecute_sql.mcp_http.create_app(extra_tools={}, adapters=None)— a composition factory that mergesextra_toolsover a copy ofTOOLS(never mutating the module global) and injects the fourports.pyadapters, defaulting to the OSS adapters whenNone.ports.Adapters— a frozen dataclass bundling the four port adapters so they pass as one argument.build_app()is retained as a thincreate_app()wrapper.Backwards compatibility
create_app()with no args behaves identically to the previousbuild_app()— same routes, sametools/list, same auth challenge.execute_sql'sinputSchemastays byte-identical.TOOLSis never mutated bycreate_app.python -m mcp_http/main()and the stdio entrypoint (mcp_harness) are unchanged.Tests
tests/test_tool_extension_seam.pycovers: no-arg parity withbuild_app, byte-identicalexecute_sqlschema, the non-mutating registry merge, the duplicate-name guard, OSS-default adapters vs. a passedAdapters(...), and the unchanged auth challenge.ruffclean; the new + existing MCP/tools/harness suites pass.