| Field | Value |
|---|---|
| RFC | 0020 |
| Title | Host-side MCP server composition |
| Status | Accepted |
| Author(s) | OpenWOP Working Group |
| Created | 2026-05-17 |
| Updated | 2026-05-18 (Active → Accepted: all 6 acceptance-criteria items satisfied. spec/v1/mcp-integration.md §"OpenWOP host as MCP server" landed at commit c5831fe (the §1-§6 buildout — mount transports, state-projection table, sampling/elicitation bridges, untrusted-boundary discipline, capability advertisement, 6 scenario references); capabilities.mcp.serverMount block in schema; SECURITY invariant mcp-server-untrusted-args; 6 scenarios (mcp-server-tool-roundtrip, mcp-server-resource-roundtrip, mcp-server-prompt-roundtrip, mcp-server-sampling-bridge, mcp-server-elicitation-bridge, mcp-server-untrusted-args) all behavioral (13/13 assertions pass); reference impl at apps/workflow-engine/backend/typescript/src/routes/mcp.ts (JSON-RPC over streamable-HTTP, env-gated OPENWOP_MCP_SERVER_ENABLED=true); CHANGELOG entry under [Unreleased].) |
| Affects | spec/v1/mcp-integration.md · schemas/capabilities.schema.json · new conformance scenarios |
| Compatibility | additive |
Summary
Adds a normative §"OpenWOP host as MCP server" section to spec/v1/mcp-integration.md, paralleling the existing §"OpenWOP host as A2A agent" treatment in a2a-integration.md. Lets an openwop host _mount_ its workflows as MCP tools/resources/prompts, with bidirectional sampling/createMessage and elicitation/create callbacks routed into the existing AI-provider and interrupt mechanisms. Pairs with the 8 core.openwop.mcp.{server-trigger,expose-,handle-,provide-roots} nodes shipped in core.openwop.mcp@1.1.0.
Motivation
spec/v1/mcp-integration.md currently covers only the _client_ direction — an openwop workflow calling out to a remote MCP server via ctx.mcp.*. The _server_ direction — an MCP-aware LLM client (Claude Desktop, Cursor, ChatGPT) discovering and invoking openwop workflows as tools — is unspecified. Track 6 of docs/PROTOCOL-GAP-CLOSURE-PLAN.md was closed on the client half only; this RFC closes the server half.
Demand signal: every other workflow editor in the 2026 catalog (Make, n8n, Zapier MCP previews) ships both directions. Without this, openwop workflows are _consumers_ of the MCP ecosystem but cannot be _contributors_ to it.
Proposal
§A New section in mcp-integration.md: "OpenWOP host as MCP server"
A normative section mirroring a2a-integration.md §"Concrete example: OpenWOP host as A2A agent":
1. Mount. A host MAY expose an MCP-server endpoint (stdio subprocess and/or streamable-HTTP). When advertised, the host serves tools/list, tools/call, resources/list, resources/templates/list, resources/read, resources/subscribe, prompts/list, prompts/get, completion/complete, ping, logging/setLevel, plus the notifications tools/list_changed, resources/list_changed, resources/updated, prompts/list_changed, message, progress, cancelled — per modelcontextprotocol.io 2025-06-18.
2. State projection: workflow → MCP tool. A workflow that registers via core.openwop.mcp.expose-tool (or the host's equivalent declarative shape) is advertised in the host's tools/list response. Each tools/call invocation starts a new openwop run with: - inputs from params.arguments validated against the tool's inputSchema. - runOptions.trustBoundary: 'untrusted' (mirrors host.mcp client-side; tool args arrive from external LLMs). - Run reaches terminal state → response body packed as CallToolResult with content[] text/image/audio parts.
3. Bidirectional callbacks. When the workflow uses core.openwop.mcp.handle-sampling, the host's MCP server bridges inbound sampling/createMessage requests into the workflow's ctx.callAI (preserves user consent + BYOK — the user's model under the user's key, never the server's). core.openwop.mcp.handle-elicitation similarly bridges elicitation/create into ctx.suspend({kind: 'clarification', profile: 'openwop-mcp-elicitation'}), returning accept/decline/cancel per the flat-schema constraint.
4. Trust boundary. All inbound MCP requests cross an untrusted boundary. Tool arguments are validated against the per-tool JSON Schema; resource URIs are sanitized; prompts are rendered without eval. The existing prompt-injection-mcp-marker invariant from SECURITY/threat-model-prompt-injection.md applies symmetrically.
§B Capability schema additions
"mcp": {
"type": "object",
"properties": {
"supported": { "type": "boolean" },
+ "serverMount": {
+ "type": "object",
+ "description": "Host advertises an MCP-server endpoint (workflows can be exposed as MCP tools / resources / prompts).",
+ "properties": {
+ "supported": { "type": "boolean" },
+ "transports": { "type": "array", "items": { "type": "string", "enum": ["stdio", "streamable-http"] } },
+ "samplingBridge": { "type": "boolean", "description": "Host bridges inbound sampling/createMessage into the workflow's ctx.callAI." },
+ "elicitationBridge": { "type": "boolean", "description": "Host bridges inbound elicitation/create into ctx.suspend." }
+ },
+ "additionalProperties": false
+ }
}
}
§C State projection table
| OpenWOP run state | MCP server response |
|---|---|
pending / running | (request blocks; SSE progress events emit notifications/progress if subscribed) |
completed | CallToolResult { content: [...], isError: false } |
failed | CallToolResult { content: [error message], isError: true } |
awaiting-input (clarification) | (out-of-band: bridged via elicitation/create callback, NOT a tools/call response) |
awaiting-input (approval) | Same — bridged via elicitation/create with accept/decline/cancel mapping |
canceled | CallToolResult { content: [...], isError: true } with tool_canceled |
§D Trust boundary
- Inbound MCP requests cross an
untrustedboundary regardless of transport.tools/call.argumentsMUST validate against the declaredinputSchema; resource URIs MUST be normalized + sandboxed; prompt arguments MUST NOT be template-evaluated. - The existing
prompt-injection-mcp-markerinvariant (SECURITY/threat-model-prompt-injection.md) applies. Outputs from an MCP tool feeding into an LLM downstream remaintrustBoundary: 'untrusted'.
§E What OpenWOP does NOT specify
Same posture as mcp-integration.md §"What openwop does NOT specify about MCP":
- Wire encoding details — those are the MCP spec.
- Specific transports beyond advertising which are supported.
- Tool authoring beyond the openwop pack contribution surface.
Compatibility
Additive. New optional capabilities.mcp.serverMount block. Existing clients that don't read it see unchanged behavior. Hosts that don't advertise it MUST NOT accept inbound MCP requests; core.openwop.mcp.server-trigger registration MUST refuse with pack_peer_dependency_missing.
Conformance
New scenarios (capability-gated on capabilities.mcp.serverMount.supported):
mcp-server-tool-roundtrip.test.ts—tools/listthentools/callagainst a workflow exposed viacore.openwop.mcp.expose-tool.mcp-server-resource-roundtrip.test.ts—resources/listthenresources/read.mcp-server-prompt-roundtrip.test.ts—prompts/listthenprompts/get.mcp-server-sampling-bridge.test.ts— inboundsampling/createMessagereaches workflow'sctx.callAI(gated onsamplingBridge).mcp-server-elicitation-bridge.test.ts— inboundelicitation/createreachesctx.suspendand the accept/decline/cancel path round-trips (gated onelicitationBridge).mcp-server-untrusted-args.test.ts— malformedargumentsrejected perinputSchema.
Alternatives considered
1. Pack-side only. Ship the core.openwop.mcp.server-* nodes without an RFC, treating server-mount as a host extension. Rejected: the bidirectional sampling/elicitation flow needs spec-level state projection because it sits at the intersection of aiProviders (BYOK consent) and interrupt (suspension semantics) — both of which are normatively openwop's. 2. Defer to v1.2. Rejected: 8 pack nodes already ship in core.openwop.mcp@1.1.0 and would otherwise be unspecified. Better to land the spec at Draft now and lock the wire shape early.
Unresolved questions
1. Should samplingBridge and elicitationBridge be independently advertisable (current proposal) or a single bidirectional: true flag? Independent feels cleaner because hosts may want to expose only one. 2. Authentication for MCP server endpoint: same auth.profiles.* surface, or MCP-specific (the MCP spec defines its own OAuth2 flow for streamable-http)? Spec the link, defer the choice to host.
Implementation notes (non-normative)
- Schema diff in §B lands in
capabilities.schema.jsonon Active promotion, not at Draft. - New SECURITY invariant proposed:
mcp-server-untrusted-args(tool args MUST validate againstinputSchemabefore workflow start). Lands alongside the matchingmcp-server-untrusted-args.test.tsat Active. - Reference impl candidate: extend
examples/hosts/postgres/with an optional MCP-server mount behindOPENWOP_MCP_SERVER_PORT=…env var.
Acceptance criteria
- [x]
mcp-integration.md§"OpenWOP host as MCP server" added (mirrors the A2A treatment). - [x]
capabilities.serverMountblock incapabilities.schema.json. - [x] SECURITY invariant
mcp-server-untrusted-args+ matching test. - [x] 6 conformance scenarios above (capability-gated) — promoted from
it.todo()to live behavioral 2026-05-17 against the workflow-engine reference host. 13/13 assertions pass. - [x] Reference host implementation —
apps/workflow-engine/backend/typescript/src/routes/mcp.tsships JSON-RPC over streamable-HTTP, env-gated onOPENWOP_MCP_SERVER_ENABLED=true. Postgres reference host implementation deferred to its own track. - [x] CHANGELOG entry —
[Unreleased]§"RFC 0020 host-side MCP server mount — behavioral conformance live (2026-05-17)".
References
spec/v1/mcp-integration.md(existing client-side coverage; this RFC adds the server-side section).spec/v1/a2a-integration.md(template for server-side composition prose).core.openwop.mcp@1.1.0pack (8 mcp.server-\* nodes that this RFC normates).- modelcontextprotocol.io 2025-06-18 spec (canonical wire reference).
SECURITY/threat-model-prompt-injection.md(untrusted-boundary invariants).core.openwop.ai@1.1.2+core.openwop.mcp@1.1.1packs (2026-05-17) — honor §D downstream by wrapping user-role content in<UNTRUSTED>...</UNTRUSTED>markers whenctx.trustBoundary === 'untrusted'. SeeCHANGELOG.md [Unreleased]§"UNTRUSTED-marker discipline" andapps/workflow-engine/backend/typescript/test/untrusted-marker.test.ts(14 in-tree tests covering both packs' delegate behavior).