Skip to content

Commit 3b3ad42

Browse files
ceilf6claude
andauthored
feat(web-fetch): wire @frontagent/mcp-web-fetch into the agent registry & planner (#357) (#359)
Wires the previously-unwired @frontagent/mcp-web-fetch adapter (PR #358) so the agent can actually call the `web_fetch` tool, and surfaces it to the two-phase planner. This is increment 3/3 of #357 and closes it. - runtime-node: register `web_fetch` in WebMCPClient.callTool + listTools, delegating to handleWebFetchTool (no browser launch — lazy playwright, so web_fetch stays decoupled from the browser tools sharing the client). - core/agent: add `web_fetch` to registerWebTools() so the executor routes it to the 'web' client (mirrors how filesense_* is routed via the 'file' client in registerFileTools()). - core/schemas: add `web_fetch` to ACTION_ENUM so planner-emitted steps with action `web_fetch` pass StepSchema validation (url param already exists in STEP_PARAMS_SCHEMA). Treated like browser_navigate. - core/plan-generation: mention web_fetch in both 可用工具 prompt regions + param hint, so the planner knows it can fetch external docs/API refs. - add @frontagent/mcp-web-fetch workspace dep to runtime-node. - add hermetic mcp-clients.test.ts asserting web_fetch is listed & routed. MVP scope only: no web_search, no when-to-search heuristics (deferred). SSRF guards live in the adapter (#358) with tests. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 18efd9b commit 3b3ad42

8 files changed

Lines changed: 62 additions & 1 deletion

File tree

packages/core/src/agent/agent.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ExecutionStep } from '@frontagent/shared';
2-
import { describe, expect, it } from 'vitest';
2+
import { describe, expect, it, vi } from 'vitest';
3+
import { Executor } from '../executor.js';
34
import { createAgent } from './agent.js';
45
import { generateOutput } from './answer-generation.js';
56

@@ -213,3 +214,25 @@ describe('createAgent', () => {
213214
expect(executorSnap.actionSkills).toBeInstanceOf(Array);
214215
});
215216
});
217+
218+
describe('registerWebTools routing contract', () => {
219+
it('routes web_fetch and the browser tools to the web client', () => {
220+
const spy = vi.spyOn(Executor.prototype, 'registerToolMapping');
221+
try {
222+
const agent = createAgent({
223+
projectRoot: '/test',
224+
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
225+
});
226+
agent.registerWebTools();
227+
const webTools = spy.mock.calls
228+
.filter(([, client]) => client === 'web')
229+
.map(([tool]) => tool);
230+
// web_fetch must route to the same 'web' client as the browser tools so
231+
// the executor can dispatch it to WebMCPClient.callTool.
232+
expect(webTools).toContain('web_fetch');
233+
expect(webTools).toContain('browser_navigate');
234+
} finally {
235+
spy.mockRestore();
236+
}
237+
});
238+
});

packages/core/src/agent/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export class FrontAgent {
269269
'scroll',
270270
'screenshot',
271271
'wait_for_selector',
272+
'web_fetch',
272273
];
273274
for (const tool of tools) {
274275
this.executor.registerToolMapping(tool, 'web');

packages/core/src/llm/plan-generation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export async function generatePlanInTwoPhases(
129129
- **browser_navigate**: 浏览器访问URL(⚠️ 使用上下文中提供的"开发服务器端口")
130130
- **browser_screenshot**: 页面截图
131131
- **get_page_structure**: 获取页面DOM结构
132+
- **web_fetch**: 抓取 URL 网页内容并清洗为纯文本(HTML→text),用于查阅库/框架文档、API 参考等外部资料
132133
133134
${PROGRESSIVE_EXPLORATION_PROTOCOL}
134135
@@ -271,6 +272,7 @@ ${options.skillContext ?? '无已激活内容技能'}
271272
如果上下文中提供了端口信息,使用 http://localhost:{端口}/
272273
- **browser_screenshot**: params 可选 fullPage: true
273274
- **get_page_structure**: params 可为空对象
275+
- **web_fetch**: params 需要 url
274276
275277
# 重要提示
276278
- create_file 和 apply_patch 必须设置 needsCodeGeneration: true
@@ -422,6 +424,7 @@ async function generatePlanSinglePhase(
422424
- **browser_navigate**: { url: "地址" }
423425
- **browser_screenshot**: { fullPage: true }
424426
- **get_page_structure**: {}
427+
- **web_fetch**: { url: "地址" }
425428
426429
${PROGRESSIVE_EXPLORATION_PROTOCOL}
427430

packages/core/src/llm/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ACTION_ENUM = [
1313
'browser_click',
1414
'browser_type',
1515
'browser_screenshot',
16+
'web_fetch',
1617
] as const;
1718

1819
const STEP_PARAMS_SCHEMA = z

packages/runtime-node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@frontagent/mcp-memory": "workspace:*",
2626
"@frontagent/mcp-shell": "workspace:*",
2727
"@frontagent/mcp-web": "workspace:*",
28+
"@frontagent/mcp-web-fetch": "workspace:*",
2829
"@frontagent/sdd": "workspace:*",
2930
"@frontagent/shared": "workspace:*",
3031
"@modelcontextprotocol/sdk": "^1.29.0",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { WebMCPClient } from './mcp-clients.js';
3+
4+
describe('WebMCPClient web_fetch wiring', () => {
5+
it('lists web_fetch as an available tool', async () => {
6+
// Construction is cheap: the playwright browser is launched lazily, not in
7+
// the constructor, so this stays hermetic (no browser, no network).
8+
const client = new WebMCPClient();
9+
const tools = await client.listTools();
10+
const names = tools.map((tool) => tool.name);
11+
12+
expect(names).toContain('web_fetch');
13+
// Sanity: the existing browser tools are still present alongside it.
14+
expect(names).toContain('browser_navigate');
15+
});
16+
17+
it('throws for an unknown web tool', async () => {
18+
const client = new WebMCPClient();
19+
await expect(client.callTool('definitely_not_a_tool', {})).rejects.toThrow(/Unknown web tool/);
20+
});
21+
});

packages/runtime-node/src/mcp-clients.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type RagQueryParams,
2222
} from '@frontagent/mcp-memory';
2323
import { type BrowserManager, createBrowserManager } from '@frontagent/mcp-web';
24+
import { handleWebFetchTool } from '@frontagent/mcp-web-fetch';
2425

2526
export class FileMCPClient implements MCPClient {
2627
private readonly snapshotManager: SnapshotManager;
@@ -143,6 +144,8 @@ export class WebMCPClient implements MCPClient {
143144
const { selector, timeout } = args as { selector: string; timeout?: number };
144145
return this.browserManager.waitForSelector(selector, timeout);
145146
}
147+
case 'web_fetch':
148+
return handleWebFetchTool(name, args);
146149
default:
147150
throw new Error(`Unknown web tool: ${name}`);
148151
}
@@ -159,6 +162,11 @@ export class WebMCPClient implements MCPClient {
159162
{ name: 'browser_scroll', description: '滚动页面' },
160163
{ name: 'browser_screenshot', description: '截取页面截图' },
161164
{ name: 'browser_wait_for_selector', description: '等待元素出现' },
165+
{
166+
name: 'web_fetch',
167+
description:
168+
'抓取 URL 网页内容并清洗为可读文本(HTML→text),用于查阅文档/API 参考;内置 SSRF 防护',
169+
},
162170
];
163171
}
164172

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)