Skip to content

Commit 803d9e2

Browse files
committed
test: expand unit tests and add MCP filter integration test
1 parent 957fc8b commit 803d9e2

File tree

3 files changed

+396
-0
lines changed

3 files changed

+396
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"examples:tools-computer-use": "pnpm -F tools start:computer-use",
3838
"examples:tools-file-search": "pnpm -F tools start:file-search",
3939
"examples:tools-web-search": "pnpm -F tools start:web-search",
40+
"examples:tool-filter": "tsx examples/mcp/tool-filter-example.ts",
4041
"ci:publish": "pnpm publish -r --no-git-checks",
4142
"bump-version": "changeset version && pnpm -F @openai/* prebuild",
4243
"prepare": "husky",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { Agent, run, setDefaultModelProvider } from '../src';
3+
import { mcpToFunctionTool } from '../src/mcp';
4+
import { NodeMCPServerStdio } from '../src/shims/mcp-server/node';
5+
import { createMCPToolStaticFilter } from '../src/mcpUtil';
6+
import { FakeModel, FakeModelProvider } from './stubs';
7+
import { Usage } from '../src/usage';
8+
import * as fs from 'node:fs';
9+
import * as path from 'node:path';
10+
11+
class StubFilesystemServer extends NodeMCPServerStdio {
12+
private dir: string;
13+
public tools: any[];
14+
constructor(dir: string, filter: any) {
15+
super({ command: 'noop', name: 'stubfs', cacheToolsList: true });
16+
this.dir = dir;
17+
this.toolFilter = filter;
18+
this.tools = [
19+
{
20+
name: 'read_file',
21+
description: '',
22+
inputSchema: {
23+
type: 'object',
24+
properties: { path: { type: 'string' } },
25+
required: ['path'],
26+
additionalProperties: false,
27+
},
28+
},
29+
{
30+
name: 'list_directory',
31+
description: '',
32+
inputSchema: {
33+
type: 'object',
34+
properties: { path: { type: 'string' } },
35+
required: ['path'],
36+
additionalProperties: false,
37+
},
38+
},
39+
{
40+
name: 'write_file',
41+
description: '',
42+
inputSchema: {
43+
type: 'object',
44+
properties: {
45+
path: { type: 'string' },
46+
text: { type: 'string' },
47+
},
48+
required: ['path', 'text'],
49+
additionalProperties: false,
50+
},
51+
},
52+
];
53+
}
54+
async connect() {}
55+
async close() {}
56+
async listTools() {
57+
return this.tools;
58+
}
59+
async callTool(name: string, args: any) {
60+
const blocked = (this.toolFilter as any)?.blockedToolNames ?? [];
61+
if (blocked.includes(name)) {
62+
return [
63+
{ type: 'text', text: `Tool "${name}" is blocked by MCP filter` },
64+
];
65+
}
66+
if (name === 'list_directory') {
67+
const files = fs.readdirSync(this.dir);
68+
return [{ type: 'text', text: files.join('\n') }];
69+
}
70+
if (name === 'read_file') {
71+
const text = fs.readFileSync(path.join(this.dir, args.path), 'utf8');
72+
return [{ type: 'text', text }];
73+
}
74+
if (name === 'write_file') {
75+
fs.writeFileSync(path.join(this.dir, args.path), args.text, 'utf8');
76+
return [{ type: 'text', text: 'ok' }];
77+
}
78+
return [];
79+
}
80+
}
81+
82+
describe('MCP tool filter integration', () => {
83+
beforeAll(() => {
84+
setDefaultModelProvider(new FakeModelProvider());
85+
});
86+
const samplesDir = path.join(__dirname, '../../../examples/mcp/sample_files');
87+
const filter = createMCPToolStaticFilter({
88+
allowed: ['read_file', 'list_directory', 'write_file'],
89+
blocked: ['write_file'],
90+
});
91+
const server = new StubFilesystemServer(samplesDir, filter);
92+
const tools = server.tools.map((t) => mcpToFunctionTool(t, server, false));
93+
94+
it('allows listing files', async () => {
95+
const modelResponses = [
96+
{
97+
output: [
98+
{
99+
id: '1',
100+
type: 'function_call',
101+
name: 'list_directory',
102+
callId: '1',
103+
status: 'completed',
104+
arguments: '{}',
105+
},
106+
],
107+
usage: new Usage(),
108+
},
109+
];
110+
const agent = new Agent({
111+
name: 'Lister',
112+
toolUseBehavior: 'stop_on_first_tool',
113+
model: new FakeModel(modelResponses),
114+
tools,
115+
});
116+
const result = await run(agent, 'List files');
117+
expect(result.finalOutput).toContain('books.txt');
118+
expect(result.finalOutput).toContain('favorite_songs.txt');
119+
});
120+
121+
it('blocks write_file', async () => {
122+
const modelResponses = [
123+
{
124+
output: [
125+
{
126+
id: '1',
127+
type: 'function_call',
128+
name: 'write_file',
129+
callId: '1',
130+
status: 'completed',
131+
arguments: '{"path":"test.txt","text":"hello"}',
132+
},
133+
],
134+
usage: new Usage(),
135+
},
136+
];
137+
const agent = new Agent({
138+
name: 'Writer',
139+
toolUseBehavior: 'stop_on_first_tool',
140+
model: new FakeModel(modelResponses),
141+
tools,
142+
});
143+
const result = await run(agent, 'write');
144+
expect(result.finalOutput).toBe(
145+
JSON.stringify({
146+
type: 'text',
147+
text: 'Tool "write_file" is blocked by MCP filter',
148+
}),
149+
);
150+
});
151+
});

0 commit comments

Comments
 (0)