4.8 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| mcp-envelope-integration | Update from_mcp adapter to use mcpEnvelope, structuredContent, and outputSchema extraction | completed |
|
moderate | medium | component | implementation |
Description
Update src/from_mcp.ts to align with the response envelope specification documented in response-envelopes.md § MCP Adapter, adapters.md § from_mcp, and the drift tables in call-protocol.md and api-surface.md.
Changes needed (current source → target):
| What | Current source | Target |
|---|---|---|
outputSchema at discovery time |
Type.Unknown() for all tools |
tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown() |
| Handler return value | Returns result.content (line 79) |
Returns mcpEnvelope(data, meta) |
structuredContent |
Not used | Prefer as data when present; fall back to mapMCPContentBlocks(result.content) |
isError handling |
Throws Error (line 76) |
Wraps in envelope with meta.isError: true, does NOT throw |
Value.Cast() |
Not used | If structuredContent && outputSchema !== Unknown, cast Value.Cast(outputSchema, structuredContent) |
Implementation details:
-
outputSchemaextraction: Whentool.outputSchemais present (MCP spec 2025-06-18+), convert it to TypeBox viaFromSchema(tool.outputSchema). Fall back toType.Unknown(). -
Handler behavior: The handler should use
mcpEnvelope()to wrap results:handler: async (input, context) => { const result = await client.callTool({ name: tool.name, arguments: input }) const data = result.structuredContent ? (spec.outputSchema !== Type.Unknown() ? Value.Cast(spec.outputSchema, result.structuredContent) : result.structuredContent) : mapMCPContentBlocks(result.content) return mcpEnvelope(data, { isError: result.isError ?? false, content: mapMCPContentBlocks(result.content), structuredContent: result.structuredContent, _meta: result._meta, }) } -
mapMCPContentBlocks(): A helper function that maps SDKContentBlock[]to ourMCPContentBlock[]types. Unknown block types are mapped to{ type: "text", text: JSON.stringify(block) }as fallback. -
isErrorno longer throws: MCP errors are represented as data, not thrown exceptions. Only transport-level errors (connection failure, tool not found) throw. -
The tool iteration needs to capture
outputSchemaper tool since the handler closure needs it forValue.Cast.
Acceptance Criteria
tool.outputSchemais converted viaFromSchema()when present; falls back toType.Unknown()- Handler returns
mcpEnvelope()— never raw content and never throws onisError structuredContentis preferred as data when presentmapMCPContentBlocks()helper maps SDK content blocks to ourMCPContentBlock[]types- Unknown MCP content block types fall back to
{ type: "text", text: JSON.stringify(block) } Value.Cast()normalization applied whenoutputSchema !== Type.Unknown()andstructuredContentis presentisError: trueresults are wrapped in envelope, not thrown- Transport-level errors (connection, etc.) still throw
CallError MCPClientWrapper.toolstype still returnsArray<OperationSpec & { handler: OperationHandler }>- Existing tests updated; new tests for envelope wrapping, structuredContent, isError handling
npm run buildpasses,npm run lintpasses,npm testpasses
References
- docs/architecture/adapters.md § from_mcp (Implementation changes needed table)
- docs/architecture/response-envelopes.md § MCP Adapter, § Handler behavior change
- docs/architecture/api-surface.md § Source vs. Spec Drift (from_mcp row)
- src/from_mcp.ts
Notes
Merged feat/response-envelope-types branch first to get ResponseEnvelope types. The Kind symbol from @alkdev/typebox is used to detect Type.Unknown() schema at runtime, with a fallback to empty-object detection.
Summary
Implemented MCP adapter envelope integration in src/from_mcp.ts:
- Created:
test/from_mcp.test.ts(20 tests) - Modified:
src/from_mcp.tsoutputSchemaextraction fromtool.outputSchemaviaFromSchema(), falls back toType.Unknown()- Handler returns
mcpEnvelope()with structured/legacy data path structuredContentpreferred asdatawhen present,Value.Cast()applied whenoutputSchemais not UnknownisError: truewrapped in envelope meta, NOT thrown- Transport-level config errors throw
CallError - Added
mapMCPContentBlocks()exported helper mapping SDKContentBlock[]to ourMCPContentBlock[] - Unknown block types fall back to
{ type: "text", text: JSON.stringify(block) } - Removed unused
OperationContextimport
- Build, lint, and all 95 tests pass