Skip to content

Commit 199a0c4

Browse files
mattrossmangregnr
andauthored
feat: onToolCall callback (#146)
* feat: `onToolCall` in mcp-utils * test: `onToolCall` in mcp-utils * feat: forward `onToolCall` through mcp-server-supabase * chore: cleanup * test: rename with "and errors" Co-authored-by: Greg Richardson <[email protected]> * fix: make `onToolCall` non-blocking * fix: no async `onToolCall` * feat: re-export `ToolCallCallback` type * feat: include arguments, data, error in tool call details --------- Co-authored-by: Greg Richardson <[email protected]>
1 parent ee4a6e2 commit 199a0c4

File tree

4 files changed

+171
-3
lines changed

4 files changed

+171
-3
lines changed

packages/mcp-server-supabase/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import packageJson from '../package.json' with { type: 'json' };
22

3+
export type { ToolCallCallback } from '@supabase/mcp-utils';
34
export type { SupabasePlatform } from './platform/index.js';
45
export {
56
createSupabaseMcpServer,

packages/mcp-server-supabase/src/server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { createMcpServer, type Tool } from '@supabase/mcp-utils';
1+
import {
2+
createMcpServer,
3+
type Tool,
4+
type ToolCallCallback,
5+
} from '@supabase/mcp-utils';
26
import packageJson from '../package.json' with { type: 'json' };
37
import { createContentApiClient } from './content-api/index.js';
48
import type { SupabasePlatform } from './platform/types.js';
@@ -44,6 +48,11 @@ export type SupabaseMcpServerOptions = {
4448
* Options: 'account', 'branching', 'database', 'debugging', 'development', 'docs', 'functions', 'storage'
4549
*/
4650
features?: string[];
51+
52+
/**
53+
* Callback for after a supabase tool is called.
54+
*/
55+
onToolCall?: ToolCallCallback;
4756
};
4857

4958
const DEFAULT_FEATURES: FeatureGroup[] = [
@@ -68,6 +77,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
6877
readOnly,
6978
features,
7079
contentApiUrl = 'https://supabase.com/docs/api/graphql',
80+
onToolCall,
7181
} = options;
7282

7383
const contentApiClientPromise = createContentApiClient(contentApiUrl, {
@@ -104,6 +114,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
104114
),
105115
]);
106116
},
117+
onToolCall,
107118
tools: async () => {
108119
const contentApiClient = await contentApiClientPromise;
109120
const tools: Record<string, Tool> = {};

packages/mcp-utils/src/server.test.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
CallToolResultSchema,
55
type CallToolRequest,
66
} from '@modelcontextprotocol/sdk/types.js';
7-
import { describe, expect, test } from 'vitest';
7+
import { describe, expect, test, vi } from 'vitest';
88
import { z } from 'zod';
99
import {
1010
createMcpServer,
@@ -114,6 +114,111 @@ describe('tools', () => {
114114
caseSensitive: false,
115115
});
116116
});
117+
118+
test('tool callback is called for success and errors', async () => {
119+
const onToolCall = vi.fn();
120+
121+
const server = createMcpServer({
122+
name: 'test-server',
123+
version: '0.0.0',
124+
onToolCall,
125+
tools: {
126+
good_tool: tool({
127+
description: 'A tool that always succeeds',
128+
annotations: {
129+
title: 'Good tool',
130+
readOnlyHint: true,
131+
},
132+
parameters: z.object({ foo: z.string() }),
133+
execute: async ({ foo }) => {
134+
return `Success: ${foo}`;
135+
},
136+
}),
137+
bad_tool: tool({
138+
description: 'A tool that always fails',
139+
annotations: {
140+
title: 'Bad tool',
141+
readOnlyHint: true,
142+
},
143+
parameters: z.object({ foo: z.string() }),
144+
execute: async ({ foo }) => {
145+
throw new Error('Failure: ' + foo);
146+
},
147+
}),
148+
},
149+
});
150+
151+
const { callTool } = await setup({ server });
152+
153+
const goodToolPromise = callTool({
154+
name: 'good_tool',
155+
arguments: { foo: 'bar' },
156+
});
157+
158+
await expect(goodToolPromise).resolves.toEqual('Success: bar');
159+
expect(onToolCall).toHaveBeenLastCalledWith({
160+
name: 'good_tool',
161+
arguments: { foo: 'bar' },
162+
annotations: {
163+
title: 'Good tool',
164+
readOnlyHint: true,
165+
},
166+
success: true,
167+
data: 'Success: bar',
168+
});
169+
170+
const badToolPromise = callTool({
171+
name: 'bad_tool',
172+
arguments: { foo: 'bar' },
173+
});
174+
175+
await expect(badToolPromise).rejects.toThrow('Failure: bar');
176+
expect(onToolCall).toHaveBeenLastCalledWith({
177+
name: 'bad_tool',
178+
arguments: { foo: 'bar' },
179+
annotations: {
180+
title: 'Bad tool',
181+
readOnlyHint: true,
182+
},
183+
success: false,
184+
error: expect.any(Error),
185+
});
186+
});
187+
188+
test("tool callback error doesn't fail the tool call", async () => {
189+
const onToolCall = vi.fn(() => {
190+
throw new Error('Tool callback failed');
191+
});
192+
193+
const server = createMcpServer({
194+
name: 'test-server',
195+
version: '0.0.0',
196+
onToolCall,
197+
tools: {
198+
good_tool: tool({
199+
description: 'A tool that always succeeds',
200+
annotations: {
201+
title: 'Good tool',
202+
readOnlyHint: true,
203+
},
204+
parameters: z.object({ foo: z.string() }),
205+
execute: async ({ foo }) => {
206+
return `Success: ${foo}`;
207+
},
208+
}),
209+
},
210+
});
211+
212+
const { callTool } = await setup({ server });
213+
214+
const goodToolPromise = callTool({
215+
name: 'good_tool',
216+
arguments: { foo: 'bar' },
217+
});
218+
219+
await expect(goodToolPromise).resolves.toEqual('Success: bar');
220+
expect(onToolCall.mock.results[0]?.type).toBe('throw');
221+
});
117222
});
118223

119224
describe('resources helper', () => {

packages/mcp-utils/src/server.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,26 @@ export type InitData = {
175175
clientCapabilities: ClientCapabilities;
176176
};
177177

178+
type ToolCallBaseDetails = {
179+
name: string;
180+
arguments: Record<string, unknown>;
181+
annotations?: Annotations;
182+
};
183+
184+
type ToolCallSuccessDetails = ToolCallBaseDetails & {
185+
success: true;
186+
data: unknown;
187+
};
188+
189+
type ToolCallErrorDetails = ToolCallBaseDetails & {
190+
success: false;
191+
error: unknown;
192+
};
193+
194+
export type ToolCallDetails = ToolCallSuccessDetails | ToolCallErrorDetails;
195+
178196
export type InitCallback = (initData: InitData) => void | Promise<void>;
197+
export type ToolCallCallback = (details: ToolCallDetails) => void;
179198
export type PropCallback<T> = () => T | Promise<T>;
180199
export type Prop<T> = T | PropCallback<T>;
181200

@@ -205,6 +224,11 @@ export type McpServerOptions = {
205224
*/
206225
onInitialize?: InitCallback;
207226

227+
/**
228+
* Callback for after a tool is called.
229+
*/
230+
onToolCall?: ToolCallCallback;
231+
208232
/**
209233
* Resources to be served by the server. These can be defined as a static
210234
* object or as a function that dynamically returns the object synchronously
@@ -452,7 +476,34 @@ export function createMcpServer(options: McpServerOptions) {
452476
.strict()
453477
.parse(request.params.arguments ?? {});
454478

455-
const result = await tool.execute(args);
479+
const executeWithCallback = async (tool: Tool) => {
480+
// Wrap success or error in a result value
481+
const res = await tool
482+
.execute(args)
483+
.then((data: unknown) => ({ success: true as const, data }))
484+
.catch((error) => ({ success: false as const, error }));
485+
486+
try {
487+
options.onToolCall?.({
488+
name: toolName,
489+
arguments: args,
490+
annotations: tool.annotations,
491+
...res,
492+
});
493+
} catch (error) {
494+
// Don't fail the tool call if the callback fails
495+
console.error('Failed to run tool callback', error);
496+
}
497+
498+
// Unwrap result
499+
if (!res.success) {
500+
throw res.error;
501+
}
502+
return res.data;
503+
};
504+
505+
const result = await executeWithCallback(tool);
506+
456507
const content = result
457508
? [{ type: 'text', text: JSON.stringify(result) }]
458509
: [];

0 commit comments

Comments
 (0)