Skip to content

Commit f8b0c1d

Browse files
authored
fix: preserve raw tool result fields (#58)
1 parent 2ed8c51 commit f8b0c1d

6 files changed

Lines changed: 69 additions & 17 deletions

File tree

.changeset/raw-tool-results.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prefecthq/fastmcp-ts": patch
3+
---
4+
5+
Preserve metadata and extension fields in client tool call results.

src/client/client.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
Root,
3939
Tool,
4040
} from './results.js'
41+
import { normalizeCallToolResult } from './results.js'
4142
import type { ClientTransportInput, McpConfig } from './transports.js'
4243
import { resolveTransport } from './transports.js'
4344
import type { MultiServerOptions } from './multi-server.js'
@@ -412,11 +413,7 @@ export class Client implements IClient {
412413
const result = await this._reauthRetry(() =>
413414
this._sdk().callTool({ name, arguments: args ?? {}, ...this._metaParams() }, sdkOptions),
414415
)
415-
return {
416-
content: result.content as ContentBlock[],
417-
structuredContent: (result.structuredContent as TData | undefined) ?? null,
418-
isError: result.isError === true,
419-
}
416+
return normalizeCallToolResult<TData>(result)
420417
}
421418

422419
// -------------------------------------------------------------------------

src/client/multi-server.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { CallToolOptions, IClient, RequestOptions } from './interfaces.js'
99
import type {
1010
CallToolResult,
1111
CompletionResult,
12-
ContentBlock,
1312
GetPromptResult,
1413
Prompt,
1514
Resource,
@@ -18,6 +17,7 @@ import type {
1817
ResourceTemplate,
1918
Tool,
2019
} from './results.js'
20+
import { normalizeCallToolResult } from './results.js'
2121
import type { McpConfig, McpServerValue } from './transports.js'
2222
import { resolveEntryTransport } from './transports.js'
2323
import type { ClientDefaultOptions } from './options.js'
@@ -267,11 +267,7 @@ export class MultiServerClient implements IClient {
267267
{ name: localName, arguments: args ?? {}, ...this._metaParamsFor(serverName) },
268268
sdkOptions,
269269
)
270-
return {
271-
content: result.content as ContentBlock[],
272-
structuredContent: (result.structuredContent as TData | undefined) ?? null,
273-
isError: result.isError === true,
274-
}
270+
return normalizeCallToolResult<TData>(result)
275271
}
276272

277273
// -------------------------------------------------------------------------

src/client/results.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { CreateMessageResult, CreateMessageResultWithTools, ContentBlock } from "@modelcontextprotocol/server";
1+
import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/client'
2+
import type { CreateMessageResult, CreateMessageResultWithTools } from "@modelcontextprotocol/server";
23

34
export type {
45
Tool,
@@ -29,14 +30,24 @@ export type {
2930
export type AnySamplingResult = CreateMessageResult | CreateMessageResultWithTools
3031
/**
3132
* The SDK's CallToolResult with a typed generic for structuredContent.
32-
* Use TData to get typed access to structured tool output.
33+
* The intersection retains its open extension fields while narrowing the two
34+
* values FastMCP normalizes.
3335
*/
34-
export type CallToolResult<TData = unknown> = {
35-
content: ContentBlock[]
36+
export type CallToolResult<TData = unknown> = SdkCallToolResult & {
3637
structuredContent: TData | null
3738
isError: boolean
3839
}
3940

41+
export function normalizeCallToolResult<TData = unknown>(
42+
result: SdkCallToolResult,
43+
): CallToolResult<TData> {
44+
return {
45+
...result,
46+
structuredContent: (result.structuredContent as TData | undefined) ?? null,
47+
isError: result.isError === true,
48+
}
49+
}
50+
4051
export type CompletionResult = {
4152
values: string[]
4253
total?: number

tests/client/multi-server.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi } from 'vitest'
22
import { z } from 'zod/v4'
3-
import { FastMCP } from 'fastmcp-ts/server'
3+
import { FastMCP, ToolResult } from 'fastmcp-ts/server'
44
import { Client, MultiServerClient } from 'fastmcp-ts/client'
55
import type { TextResourceContents } from "@modelcontextprotocol/server";
66

@@ -207,6 +207,24 @@ describe('Client — Multi-server', () => {
207207
const result = await client.callToolRaw('err_fail')
208208
expect(result.isError).toBe(true)
209209
})
210+
211+
it('callToolRaw() preserves metadata and extension fields', async () => {
212+
const mcp = new FastMCP({ name: 'extended', version: '1.0.0' })
213+
mcp.tool(
214+
{ name: 'result', description: 'extended result', input: z.object({}) },
215+
() =>
216+
new ToolResult({
217+
content: [{ type: 'text', text: 'extended' }],
218+
_meta: { 'com.example/trace': 'trace-123' },
219+
'com.example/result': { display: 'card' },
220+
}),
221+
)
222+
await using client = await MultiServerClient.connect({ mcpServers: { extended: mcp } })
223+
224+
const result = await client.callToolRaw('extended_result')
225+
expect(result._meta).toMatchObject({ 'com.example/trace': 'trace-123' })
226+
expect(result['com.example/result']).toEqual({ display: 'card' })
227+
})
210228
})
211229

212230
// -------------------------------------------------------------------------

tests/client/tools.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi } from 'vitest'
22
import { z } from 'zod/v4'
3-
import { FastMCP, Image } from 'fastmcp-ts/server'
3+
import { FastMCP, Image, ToolResult } from 'fastmcp-ts/server'
44
import { Client, ToolCallError } from 'fastmcp-ts/client'
55
import type { Tool } from 'fastmcp-ts/client'
66

@@ -141,6 +141,31 @@ describe('Client — Tools', () => {
141141
},
142142
)
143143
})
144+
145+
it('preserves metadata and extension fields', async () => {
146+
await withServer(
147+
(mcp) => {
148+
mcp.tool(
149+
{ name: 'extended', description: 'a tool', input: z.object({}) },
150+
() =>
151+
new ToolResult({
152+
content: [{ type: 'text', text: 'extended' }],
153+
_meta: { 'com.example/trace': 'trace-123' },
154+
'com.example/result': { display: 'card' },
155+
}),
156+
)
157+
},
158+
async (client) => {
159+
const raw = await client.callToolRaw('extended', {})
160+
expect(raw._meta).toMatchObject({ 'com.example/trace': 'trace-123' })
161+
expect(raw['com.example/result']).toEqual({ display: 'card' })
162+
163+
const result = await client.callTool('extended', {})
164+
expect(result._meta).toMatchObject({ 'com.example/trace': 'trace-123' })
165+
expect(result['com.example/result']).toEqual({ display: 'card' })
166+
},
167+
)
168+
})
144169
})
145170
})
146171

0 commit comments

Comments
 (0)