Skip to content

feat(mcp): tool-extension + adapter-injection seam for the HTTP server - #100

Merged
vishalkalbi27 merged 3 commits into
mainfrom
spec/AH-001-tool-extension-seam
Jul 10, 2026
Merged

vishalkalbi27 merged 3 commits into
mainfrom
spec/AH-001-tool-extension-seam

Conversation

@vishalkalbi27

Copy link
Copy Markdown
Collaborator

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 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 dataclass bundling the four port adapters so they pass as one argument.
  • build_app() is retained as a thin create_app() wrapper.

Backwards compatibility

  • create_app() with no args behaves identically to the previous build_app() — same routes, same tools/list, same auth challenge.
  • execute_sql's inputSchema stays byte-identical.
  • The module-global TOOLS is never mutated by create_app.
  • python -m mcp_http / main() and the stdio entrypoint (mcp_harness) are unchanged.

Tests

tests/test_tool_extension_seam.py covers: no-arg parity with build_app, byte-identical execute_sql schema, the non-mutating registry merge, the duplicate-name guard, OSS-default adapters vs. a passed Adapters(...), and the unchanged auth challenge. ruff clean; the new + existing MCP/tools/harness suites pass.

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>
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

All contributors have signed the CLA. Thank you!
Posted by the CLA Assistant Lite bot.

@vishalkalbi27

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jul 10, 2026
@vishalkalbi27

Copy link
Copy Markdown
Collaborator Author

recheck

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shared TOOLS registry with a duplicate-name guard.
  • Introduces ports.Adapters (frozen dataclass) to bundle the four port adapters.
  • Refactors mcp_http to provide create_app(extra_tools, adapters) (keeping build_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 thread packages/agami-core/src/mcp_http.py Outdated
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 thread packages/agami-core/src/mcp_http.py Outdated
Comment thread packages/agami-core/src/mcp_http.py Outdated
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}
Comment thread packages/agami-core/src/mcp_http.py Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

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>
@vishalkalbi27
vishalkalbi27 merged commit c444031 into main Jul 10, 2026
6 checks passed
@vishalkalbi27
vishalkalbi27 deleted the spec/AH-001-tool-extension-seam branch July 10, 2026 12:53
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants