diff --git a/docs/content/1.guide/15.agent-native.md b/docs/content/1.guide/15.agent-native.md index 6953bfd1..168a7506 100644 --- a/docs/content/1.guide/15.agent-native.md +++ b/docs/content/1.guide/15.agent-native.md @@ -189,6 +189,29 @@ ctx.agent.registerTool({ }) ``` +## Returning images and other content + +The MCP adapter sends a tool result as one text block that holds the result as JSON. To send other MCP content, such as a screenshot next to its description, attach content blocks to the result with `withMcpContent`: + +```ts +import { defineRpcFunction, withMcpContent } from 'devframe' + +export const captureComponent = defineRpcFunction({ + name: 'my-plugin:capture-component', + type: 'query', + agent: { description: 'Take a screenshot of one component. Call it when the layout of the component matters.' }, + handler: async () => { + const { markdown, png } = await capture() + return withMcpContent({ markdown }, [ + { type: 'text', text: markdown }, + { type: 'image', data: png, mimeType: 'image/png' }, + ]) + }, +}) +``` + +The adapter sends the blocks in place of the JSON text block. A block is `text`, `image` or `audio`, and `data` is base64. The blocks are stored under a hidden key, so the returned value does not change: the SPA gets `{ markdown }` over RPC, without the image data. If the tool has an output schema, `structuredContent` is also that plain value. Invalid blocks make the call fail with [`DF0081`](/errors/DF0081). + ## Structured errors A coded diagnostic thrown from a handler crosses the MCP boundary as JSON: diff --git a/docs/content/6.errors/DF0081.md b/docs/content/6.errors/DF0081.md new file mode 100644 index 00000000..8b27e640 --- /dev/null +++ b/docs/content/6.errors/DF0081.md @@ -0,0 +1,33 @@ +--- +title: 'DF0081: Invalid MCP Content Blocks' +description: 'Tool "{name}" returned invalid MCP content blocks: {reason}' +--- + +## Message + +> Tool "`{name}`" returned invalid MCP content blocks: `{reason}` + +## Cause + +A tool result carries content blocks from `withMcpContent`, but the blocks are not a non-empty array of valid blocks. A valid block is `{ type: 'text', text }`, `{ type: 'image', data, mimeType }` or `{ type: 'audio', data, mimeType }`, and each field is a string. The MCP adapter does not send invalid blocks. It returns this error as an `isError` result of the `tools/call` request. + +## Example + +```ts +import { withMcpContent } from 'devframe' + +ctx.agent.registerTool({ + id: 'my-plugin:snapshot', + description: 'Take a snapshot.', + // The image block has no `mimeType`. + handler: () => withMcpContent({}, [{ type: 'image', data: png }]), +}) +``` + +## Fix + +Give `withMcpContent` at least one block, and give each block all of its fields: `text` for a text block, base64 `data` and a `mimeType` (for example `image/png`) for an image or audio block. + +## Source + +- [`packages/agentic/src/mcp/build-server.ts`](https://github.com/devframes/devframe/blob/main/packages/agentic/src/mcp/build-server.ts): `mcpContentOf()` throws this when the content blocks of a `tools/call` result are invalid. diff --git a/docs/content/6.errors/index.md b/docs/content/6.errors/index.md index eee53bd3..684cce83 100644 --- a/docs/content/6.errors/index.md +++ b/docs/content/6.errors/index.md @@ -86,6 +86,7 @@ Emitted by `devframe`: the framework-neutral host, RPC, streaming, assets, servi | [DF0077](/errors/DF0077) | error | In-Page Channel Function Not Registered | | [DF0078](/errors/DF0078) | warn | Agent Surface Without @devframes/agentic | | [DF0079](/errors/DF0079) | error | MCP Enabled Without @devframes/agentic | +| [DF0081](/errors/DF0081) | error | Invalid MCP Content Blocks | ## Hub: context & lifecycle (DF80xx) diff --git a/packages/agentic/src/mcp/__tests__/mcp-http.test.ts b/packages/agentic/src/mcp/__tests__/mcp-http.test.ts index 709109b9..98b0622d 100644 --- a/packages/agentic/src/mcp/__tests__/mcp-http.test.ts +++ b/packages/agentic/src/mcp/__tests__/mcp-http.test.ts @@ -1,10 +1,13 @@ import type { StartedServer } from 'devframe/internal' import type { DevframeDefinition, McpRouteOptions } from 'devframe/types' import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client' +import { withMcpContent } from 'devframe' import { createDevServer } from 'devframe/adapters/dev' import { afterEach, describe, expect, it } from 'vitest' const TOKEN = 'a-high-entropy-test-bearer-token' +/** A 1x1 PNG. */ +const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==' function defineTestDef(overrides?: Partial): DevframeDefinition { return { @@ -95,6 +98,38 @@ describe('mcp adapter (streamable http route)', () => { } }) + it('sends the content blocks attached with withMcpContent', async () => { + const started = await boot(true, defineTestDef({ + setup(ctx) { + ctx.agent.registerTool({ + id: 'snapshot', + description: 'Take a snapshot.', + safety: 'read', + handler: () => withMcpContent({ markdown: 'A red pixel.' }, [ + { type: 'text', text: 'A red pixel.' }, + { type: 'image', data: PNG_BASE64, mimeType: 'image/png' }, + ]), + }) + }, + })) + const client = new Client( + { name: 'test-client', version: '0.0.0' }, + { versionNegotiation: { mode: 'auto' } }, + ) + try { + await client.connect(originTransport(started)) + const result = await client.callTool({ name: 'snapshot', arguments: {} }) + expect(result.isError).toBeFalsy() + expect(result.content).toEqual([ + { type: 'text', text: 'A red pixel.' }, + { type: 'image', data: PNG_BASE64, mimeType: 'image/png' }, + ]) + } + finally { + await client.close() + } + }) + it('answers a bare GET with 405 (no session lifecycle)', async () => { const started = await boot() // Stateless serving has no session stream to open, so the SDK answers a diff --git a/packages/agentic/src/mcp/__tests__/mcp-server.test.ts b/packages/agentic/src/mcp/__tests__/mcp-server.test.ts index 21b7a9f0..93b88743 100644 --- a/packages/agentic/src/mcp/__tests__/mcp-server.test.ts +++ b/packages/agentic/src/mcp/__tests__/mcp-server.test.ts @@ -1,10 +1,27 @@ -import type { DevframeHost } from 'devframe/types' +import type { StandardSchemaV1 } from '@standard-schema/spec' +import type { DevframeHost, McpContentBlock } from 'devframe/types' import { Client, InMemoryTransport } from '@modelcontextprotocol/client' +import { withMcpContent } from 'devframe' import { createHostContext } from 'devframe/node' import * as v from 'valibot' import { describe, expect, it } from 'vitest' import { buildMcpServerFromContext } from '../build-server' +/** A 1x1 PNG. */ +const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==' + +/** A Standard Schema that accepts any value and converts to `jsonSchema`. */ +function objectSchema(jsonSchema: Record): StandardSchemaV1 { + return { + '~standard': { + version: 1, + vendor: 'test', + validate: (value: unknown) => ({ value }), + jsonSchema: { input: () => jsonSchema, output: () => jsonSchema }, + } as StandardSchemaV1['~standard'], + } +} + function nullHost(): DevframeHost { return { mountStatic: () => { /* no-op */ }, @@ -166,6 +183,103 @@ describe('mcp adapter (in-memory)', () => { } }) + it('sends the content blocks attached with withMcpContent in place of the JSON text', async () => { + const { ctx, client, cleanup } = await bootPair() + try { + ctx.agent.registerTool({ + id: 'snapshot', + description: 'Take a snapshot.', + handler: () => withMcpContent({ markdown: 'A red pixel.' }, [ + { type: 'text', text: 'A red pixel.' }, + { type: 'image', data: PNG_BASE64, mimeType: 'image/png' }, + { type: 'audio', data: 'AAAA', mimeType: 'audio/wav' }, + ]), + }) + + const result = await client.callTool({ name: 'snapshot', arguments: {} }) + expect(result.isError).toBeFalsy() + expect(result.content).toEqual([ + { type: 'text', text: 'A red pixel.' }, + { type: 'image', data: PNG_BASE64, mimeType: 'image/png' }, + { type: 'audio', data: 'AAAA', mimeType: 'audio/wav' }, + ]) + expect(result.structuredContent).toBeUndefined() + } + finally { + await cleanup() + } + }) + + it('keeps the plain result as structured content for an rpc-backed tool with an output schema', async () => { + const { ctx, client, cleanup } = await bootPair() + try { + ctx.rpc.register({ + name: 'snapshot', + type: 'query', + jsonSerializable: true, + args: [], + returns: objectSchema({ type: 'object', properties: { markdown: { type: 'string' } }, required: ['markdown'] }), + agent: { description: 'Take a snapshot.' }, + handler: () => withMcpContent({ markdown: 'A red pixel.' }, [ + { type: 'image', data: PNG_BASE64, mimeType: 'image/png' }, + ]), + } as never) + + const listed = await client.listTools() + const tool = listed.tools.find(t => t.name.endsWith('snapshot'))! + expect(tool.outputSchema).toBeDefined() + + const result = await client.callTool({ name: tool.name, arguments: {} }) + expect(result.isError).toBeFalsy() + expect(result.content).toEqual([{ type: 'image', data: PNG_BASE64, mimeType: 'image/png' }]) + expect(result.structuredContent).toEqual({ markdown: 'A red pixel.' }) + } + finally { + await cleanup() + } + }) + + it('returns DF0081 when the attached content blocks are invalid', async () => { + const { ctx, client, cleanup } = await bootPair() + try { + ctx.agent.registerTool({ + id: 'broken', + description: 'Returns a bad block.', + handler: () => withMcpContent({}, [{ type: 'image', data: PNG_BASE64 } as never]), + }) + + const result = await client.callTool({ name: 'broken', arguments: {} }) + expect(result.isError).toBe(true) + const content = result.content as Array<{ type: string, text: string }> + expect(content[0]!.text).toContain('DF0081') + } + finally { + await cleanup() + } + }) + + it('returns DF0081 when the attached content blocks have holes', async () => { + const { ctx, client, cleanup } = await bootPair() + try { + // eslint-disable-next-line unicorn/no-new-array + const sparse = new Array(2) as McpContentBlock[] + sparse[1] = { type: 'text', text: 'second' } + ctx.agent.registerTool({ + id: 'sparse', + description: 'Returns a sparse block array.', + handler: () => withMcpContent({}, sparse), + }) + + const result = await client.callTool({ name: 'sparse', arguments: {} }) + expect(result.isError).toBe(true) + const content = result.content as Array<{ type: string, text: string }> + expect(content[0]!.text).toContain('DF0081') + } + finally { + await cleanup() + } + }) + it('calls an rpc-backed tool whose return schema has no native converter', async () => { const { ctx, client, cleanup } = await bootPair() try { diff --git a/packages/agentic/src/mcp/build-server.ts b/packages/agentic/src/mcp/build-server.ts index 0860f5da..382fdef0 100644 --- a/packages/agentic/src/mcp/build-server.ts +++ b/packages/agentic/src/mcp/build-server.ts @@ -1,12 +1,12 @@ import type { Tool } from '@modelcontextprotocol/server' import type { StandardSchemaV1 } from '@standard-schema/spec' import type { RpcFunctionDefinitionAnyWithContext } from 'devframe/rpc' -import type { AgentTool, CreateMcpServerOptions, DevframeDefinition, DevframeHost, DevframeNodeContext, McpServerHandle } from 'devframe/types' +import type { AgentTool, CreateMcpServerOptions, DevframeDefinition, DevframeHost, DevframeNodeContext, McpContentBlock, McpServerHandle } from 'devframe/types' import { homedir } from 'node:os' import process from 'node:process' import { Server } from '@modelcontextprotocol/server' import { DEVFRAME_EVENTS } from 'devframe/constants' -import { argsToJsonSchema, diagnostics, formatMcpError, returnToJsonSchema, stringifyForMcp } from 'devframe/internal' +import { argsToJsonSchema, diagnostics, formatMcpError, getMcpContent, returnToJsonSchema, stringifyForMcp } from 'devframe/internal' import { createHostContext } from 'devframe/node' import { toAgentToolName } from 'devframe/utils/agent-tool-name' import { join } from 'pathe' @@ -273,13 +273,12 @@ function registerToolHandlers( ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : undefined const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {}) + // Blocks attached with `withMcpContent` replace the default JSON text + // block. `structuredContent` stays the plain result either way, so a + // tool with an output schema still matches it. + const content = mcpContentOf(name, result) ?? [{ type: 'text', text: stringifyForMcp(result) }] return { - content: [ - { - type: 'text', - text: stringifyForMcp(result), - }, - ], + content, ...(outputSchema ? { structuredContent: result as Record } : {}), } } @@ -368,6 +367,29 @@ function registerResourceHandlers( }) } +/** + * The content blocks a tool result carries through `withMcpContent`, checked + * and copied field by field, or `undefined` when it carries none. Invalid + * blocks throw DF0081, which the `tools/call` handler turns into an + * `isError` result. + */ +function mcpContentOf(name: string, result: unknown): McpContentBlock[] | undefined { + const blocks = getMcpContent(result) + if (blocks === undefined) + return undefined + if (!Array.isArray(blocks) || blocks.length === 0) + throw diagnostics.DF0081({ name, reason: 'expected a non-empty array' }) + // `Array.from` visits the holes of a sparse array as `undefined`, so they fail the check below. + return Array.from(blocks, (block: unknown, index): McpContentBlock => { + const b = (block ?? {}) as Record + if (b.type === 'text' && typeof b.text === 'string') + return { type: 'text', text: b.text } + if ((b.type === 'image' || b.type === 'audio') && typeof b.data === 'string' && typeof b.mimeType === 'string') + return { type: b.type, data: b.data, mimeType: b.mimeType } + throw diagnostics.DF0081({ name, reason: `block ${index} is not a valid text, image or audio block` }) + }) +} + /** * MCP constrains a tool's `outputSchema` to a JSON Schema of `type: * "object"`; clients (the SDK included) reject anything else. Non-object diff --git a/packages/devframe/src/agent/__tests__/mcp-content.test.ts b/packages/devframe/src/agent/__tests__/mcp-content.test.ts new file mode 100644 index 00000000..c219a7c2 --- /dev/null +++ b/packages/devframe/src/agent/__tests__/mcp-content.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { getMcpContent, withMcpContent } from '../mcp-content' + +describe('withMcpContent', () => { + const blocks = [ + { type: 'text', text: 'A red pixel.' }, + { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }, + ] as const + + it('attaches the blocks without changing the value', () => { + const value = { markdown: 'A red pixel.' } + const result = withMcpContent(value, blocks) + expect(result).toBe(value) + expect(getMcpContent(result)).toBe(blocks) + expect(Object.keys(result)).toEqual(['markdown']) + expect(JSON.stringify(result)).toBe('{"markdown":"A red pixel."}') + expect(structuredClone(result)).toEqual({ markdown: 'A red pixel.' }) + expect(getMcpContent(structuredClone(result))).toBeUndefined() + }) + + it('reads nothing from values without blocks', () => { + expect(getMcpContent({ markdown: 'x' })).toBeUndefined() + expect(getMcpContent('text')).toBeUndefined() + expect(getMcpContent(null)).toBeUndefined() + expect(getMcpContent(undefined)).toBeUndefined() + }) +}) diff --git a/packages/devframe/src/agent/mcp-content.ts b/packages/devframe/src/agent/mcp-content.ts new file mode 100644 index 00000000..b768300c --- /dev/null +++ b/packages/devframe/src/agent/mcp-content.ts @@ -0,0 +1,42 @@ +import type { McpContentBlock } from '../types/mcp' + +/** + * Registry symbol for the content blocks attached by {@link withMcpContent}. + * `Symbol.for` keeps the key the same across duplicate copies of devframe. + */ +const MCP_CONTENT = Symbol.for('devframe:mcp-content') + +/** + * Attach MCP content blocks to a tool result, for example an image next to + * its text. The MCP adapter sends these blocks as the `content` of the + * `tools/call` result, in place of the default single JSON text block. + * + * The blocks are stored under a non-enumerable symbol key, so `value` itself + * does not change: the RPC transport to the SPA, `returns` validation, and the + * `structuredContent` of a tool with an output schema all see the plain value + * and never the (often large) block data. + * + * `value` must be an object that is not frozen. Returns `value`. + */ +export function withMcpContent(value: T, content: readonly McpContentBlock[]): T { + Object.defineProperty(value, MCP_CONTENT, { + value: content, + enumerable: false, + configurable: true, + writable: true, + }) + return value +} + +/** + * Read the blocks attached by {@link withMcpContent}, or `undefined` when the + * value carries none. The value is not validated here; the MCP adapter checks + * each block before it sends it. + * + * @internal + */ +export function getMcpContent(value: unknown): unknown { + if (value === null || typeof value !== 'object') + return undefined + return (value as Record)[MCP_CONTENT] +} diff --git a/packages/devframe/src/index.ts b/packages/devframe/src/index.ts index d3869bee..8248cf90 100644 --- a/packages/devframe/src/index.ts +++ b/packages/devframe/src/index.ts @@ -2,5 +2,6 @@ // `defineRpcFunction`) as values, plus every public type. `devframe/types` // remains available as the type-only subpath, but `devframe` is the canonical // import for both values and types. +export { withMcpContent } from './agent/mcp-content' export * from './define' export type * from './types' diff --git a/packages/devframe/src/internal/index.ts b/packages/devframe/src/internal/index.ts index c49ce87a..b370be34 100644 --- a/packages/devframe/src/internal/index.ts +++ b/packages/devframe/src/internal/index.ts @@ -39,10 +39,13 @@ // `formatMcpError`: the pure agent-surface projections shared between the // browser WebMCP registration and `@devframes/agentic/mcp`, so the two // surfaces cannot drift. +// - `getMcpContent`: reads the content blocks that `withMcpContent` (from +// `devframe`) attaches to a tool result, for `@devframes/agentic/mcp`. // - `probeDevframeOrigin`: the registry's origin-candidate probe, // `@devframes/agentic/connect` reuses it for explicit `--port` probes. export { loadAutoMcpAdapter, normalizeBasePath, resolveBasePath, resolveMcpConfig } from '../adapters/_shared' export type { ResolvedMcpConfig } from '../adapters/_shared' +export { getMcpContent } from '../agent/mcp-content' export { formatMcpError, stringifyForMcp } from '../agent/stringify' export { argsToJsonSchema, returnToJsonSchema } from '../agent/to-json-schema' export { importAgenticMcp } from '../node/agentic' diff --git a/packages/devframe/src/node/diagnostics.ts b/packages/devframe/src/node/diagnostics.ts index 7a5b9a95..b05c5825 100644 --- a/packages/devframe/src/node/diagnostics.ts +++ b/packages/devframe/src/node/diagnostics.ts @@ -220,5 +220,10 @@ export const diagnostics = defineDiagnostics({ `The \`mcp\` option is enabled, but the optional peer "@devframes/agentic" could not be loaded: ${p.reason}`, fix: 'Install `@devframes/agentic` next to devframe (the MCP adapter and the MCP SDK live there), or remove the explicit `mcp` setting.', }, + DF0081: { + why: (p: { name: string, reason: string }) => + `Tool "${p.name}" returned invalid MCP content blocks: ${p.reason}`, + fix: 'Pass `withMcpContent` a non-empty array of `{ type: \'text\', text }`, `{ type: \'image\', data, mimeType }` or `{ type: \'audio\', data, mimeType }` blocks, with string fields and base64 `data`.', + }, }, }) diff --git a/packages/devframe/src/types/mcp.ts b/packages/devframe/src/types/mcp.ts index cdb9e003..4b350563 100644 --- a/packages/devframe/src/types/mcp.ts +++ b/packages/devframe/src/types/mcp.ts @@ -33,6 +33,16 @@ export interface CreateMcpServerOptions { onReady?: (info: { transport: 'stdio' }) => void } +/** + * One MCP content block in a tool result. Attach blocks to a tool's return + * value with `withMcpContent` (from `devframe`) to send them in place of the + * default JSON text block. `data` is base64. + */ +export type McpContentBlock + = | { type: 'text', text: string } + | { type: 'image', data: string, mimeType: string } + | { type: 'audio', data: string, mimeType: string } + export interface McpServerHandle { stop: () => Promise } diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index f54ac769..186f426e 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -553,6 +553,18 @@ export type DevframeSnapshotRpcEntry = string | { export type DevframeSnapshotRpcInputs = readonly (readonly unknown[])[] | ((_: DevframeNodeContext) => readonly (readonly unknown[])[] | Promise); export type DevframeStorageScope = 'workspace' | 'project' | 'global'; export type McpAuthorization = string | ((_: Request) => boolean | Promise) | false; +export type McpContentBlock = { + type: 'text'; + text: string; +} | { + type: 'image'; + data: string; + mimeType: string; +} | { + type: 'audio'; + data: string; + mimeType: string; +}; export type McpSetting = boolean | 'auto' | McpRouteOptions; export type RemoteAssetsProvider = 'jsdelivr' | 'unpkg' | RemoteAssetsProviderCustom; export type RpcFunctionsHost = RpcFunctionsCollectorBase & { @@ -572,6 +584,7 @@ export type StaticAssetsSource = string | RemoteAssets; // #region Functions export declare function defineDevframe(_: DevframeDefinition): DevframeDefinition; +export declare function withMcpContent(_: T, _: readonly McpContentBlock[]): T; // #endregion // #region Variables diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.js b/tests/__snapshots__/tsnapi/devframe/index.snapshot.js index e681572e..a936de49 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.js @@ -3,6 +3,7 @@ */ // #region Functions export function defineDevframe(_) {} +export function withMcpContent(_, _) {} // #endregion // #region Variables diff --git a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts index 4c08e1e9..eadd1d99 100644 --- a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts @@ -371,6 +371,13 @@ export declare const diagnostics: import("nostics").Diagnostics<{ }) => string; readonly fix: "Install `@devframes/agentic` next to devframe (the MCP adapter and the MCP SDK live there), or remove the explicit `mcp` setting."; }; + readonly DF0081: { + readonly why: (p: { + name: string; + reason: string; + }) => string; + readonly fix: "Pass `withMcpContent` a non-empty array of `{ type: 'text', text }`, `{ type: 'image', data, mimeType }` or `{ type: 'audio', data, mimeType }` blocks, with string fields and base64 `data`."; + }; }, readonly [(d: import("nostics").Diagnostic, { method }?: { method?: "log" | "warn" | "error"; }) => void], never>; @@ -385,6 +392,7 @@ export { createInstanceShell } export { CreateInstanceShellOptions } export { DevframeInstanceRecord } export { DevframeInstanceRegistration } +export { getMcpContent } export { importAgenticMcp } export { InstanceRegisterConfig } export { InstanceShell } diff --git a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js index 30cfdb0d..d5d4cefa 100644 --- a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js @@ -15,6 +15,7 @@ export { createInstanceShell } export { createRpcWireCodec } export { DevframeAgentHost } export { diagnostics } +export { getMcpContent } export { importAgenticMcp } export { importRuntimeModule } export { listLiveDevframeInstances } diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 2604d37f..23f4eea7 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -69,6 +69,7 @@ export { EventsMap } export { EventUnsubscribe } export { McpAuthorization } export { McpConnectionInfo } +export { McpContentBlock } export { McpFetchHandler } export { McpRouteOptions } export { McpServerHandle }