feat(mcp): host an MCP server for Unstract API deployments#2207
feat(mcp): host an MCP server for Unstract API deployments#2207hari-kuriakose wants to merge 2 commits into
Conversation
Exposes an Unstract API deployment to coding agents over the Model Context
Protocol, so an agent can run document extraction as a tool call instead of
hand-rolling HTTP requests.
Follows the hosted-MCP pattern used in the mfbt backend: a hand-rolled
JSON-RPC 2.0 endpoint mounted in the existing app (not a separate service),
with a declarative tool registry behind it.
Endpoint mirrors the deployment's own REST URL and reuses its API key, so
there is no second credential to mint or revoke:
POST /deployment/api/<org_name>/<api_name>/ # REST
POST /mcp/<org_name>/<api_name>/ # MCP
Tools: readMeFirst, getApiInfo, extractDocument, getExecutionStatus.
Auth goes through the same DeploymentHelper validation the REST endpoint
uses, and execution through ExecutionRequestSerializer, so the MCP surface
cannot drift from the REST one on who is allowed in or what input is valid.
All auth failures answer identically (401, no detail) so the endpoint cannot
be used to enumerate deployment names.
OAuth 2.1 with dynamic client registration is deliberately not implemented;
bearer auth covers Claude Code and API clients, and OAuth would be additive.
Tests: 20 passing (protocol/dispatch in the unit tier, auth boundary in the
integration tier). Registers the mcp-server-auth critical path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transport and auth tests never reached the extraction helpers on a successful call, so the mapping from tool kwargs into execute_workflow was executed by nothing — a renamed kwarg would have passed every test and failed on the first real extraction. Adding that coverage surfaced three real problems: - Tool descriptions promised documents could be passed as any reachable URL. The execution serializer accepts *only* S3 pre-signed URLs, so an agent following the description would have failed every call. Descriptions, JSON schema and README now state the S3 requirement. - The tags schema advertised an unbounded array; the serializer caps it at one. Both limits are now sourced from the serializers rather than restated, so the advertised schema tracks what is actually enforced. - Documents were downloaded before the rate-limit slot was taken, so a call about to be rejected still pulled every document over the network. The slot is now acquired first, with the fetch inside the try block so a failed fetch still releases it. Also converts a RateLimitExceeded raised from deeper in the stack into an agent-readable message instead of letting it reach the generic "failed unexpectedly" branch. Tests: 34 passing (was 20). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |



What
Hosts an MCP server for the Unstract platform, so coding agents can run document
extraction as a tool call instead of hand-rolling HTTP requests against an API
deployment.
Modelled on how the mfbt backend hosts its MCP server (
Zipstack/mfbt,backend/app/routers/mcp_http.py): a hand-rolled JSON-RPC 2.0 endpoint mountedin the existing app, with a tool registry behind it. Same pattern, adapted from
FastAPI to Django/DRF.
How it mirrors mfbt
MCP_TOOLSname→callable dictMCPToolRegistry, declarative + JSON schemamfbt also ships a stdio server (
mcp_server.py, FastMCP). That is not thehosted path, so it is not what this replicates.
Endpoint
An MCP session is scoped to one API deployment and mirrors that deployment's
REST URL:
Auth is the deployment's existing API key — same key, same management UI.
There is no second credential to mint or revoke.
A
/<api_key>path variant exists for MCP clients that cannot set headers(mfbt carries the same workaround).
Tools
readMeFirstgetApiInfoextractDocumentgetExecutionStatusDesign decisions
DeploymentHelper— the same validation the RESTendpoint uses, so the two surfaces cannot drift on who is allowed in.
Org scoping falls out of this: a valid key for org A cannot reach org B's
deployment (covered by the
wrong orgcase).ExecutionRequestSerializer— URL validation(S3-only, HTTPS-only) and the file-count cap live there. Reimplementing them
would let the MCP surface silently diverge from REST on what input is safe.
cannot be used to enumerate deployment names.
isError: true, not protocolerrors — clients treat protocol errors as unrecoverable transport faults,
whereas an agent-fixable problem should be readable and retryable.
Tests
34 passing. Protocol/dispatch and tool logic in the unit tier; the auth
boundary in the integration tier. Registers a new
mcp-server-authcriticalpath in
tests/critical_paths.yaml.Writing the happy-path tests caught three real bugs, now fixed:
accepts only S3 pre-signed URLs, so an agent following the description
would have failed every call.
tagsschema advertised an unbounded array; the serializer caps it at 1.call about to be rejected still pulled every document over the network.
Verified locally against Postgres: full backend unit tier 306 passed, no
regressions. The 35 integration failures in
workflow_manager/execution/are pre-existing in my sandbox (no Redis) — theidentical 35 fail on the base commit.
Not implemented / notes for review
mfbt implements it for the claude.ai one-click browser connector; bearer auth
covers Claude Code and API clients. Adding it later is additive — discovery
endpoints alongside this router, no transport change.
DeploymentHelperboundary, and a full MCP session (initialize → tools/list →tools/call) was exercised end-to-end against a real DB during development, but
nothing has run a real extraction through this path. Worth one manual check
against a live deployment before merge.
backend/base_urls.py, the onlyROOT_URLCONFin this repo.cloud_base_urls.pyexists in the cloud overlaybut not here — someone with cloud context should confirm the endpoint mounts
there too.
2024-11-05and does not negotiate againstthe client's requested version (mfbt does the same). Fine for current clients.
🤖 Generated with Claude Code