跳转到正文
莫尔索随笔
返回

如果你不用 MCP 会怎样?

预计 5 分钟
编辑此页

第一时间捕获有价值的信号

本文译自 What if you don’t need MCP at all?。本文深入分析了 MCP 服务器的上下文开销问题,并展示了如何通过 Bash CLI 工具和代码来构建更灵活、更高效的 Agent 工具生态系统。

不需要 MCP

经过几个月的 Agent 编码狂热,Twitter 上仍然充斥着关于 MCP 服务器的讨论。我之前做过一些非常轻量的基准测试,看看 Bash 工具还是 MCP 服务器更适合特定任务。简而言之:如果你用心设计,两者都可以高效。

不幸的是,许多最流行的 MCP 服务器对于特定任务来说效率很低。它们需要覆盖所有基础,这意味着它们提供大量带有冗长描述的工具,消耗大量上下文。

扩展现有 MCP 服务器也很难。你可以 checkout 源代码并修改它,但然后你必须和你的 Agent 一起理解代码库。

MCP 服务器也不可组合。MCP 服务器返回的结果必须通过 Agent 的上下文才能持久化到磁盘或与其他结果组合。

我是个简单的人,所以我喜欢简单的东西。Agent 可以很好地运行 Bash 和写代码。Bash 和代码是可组合的。那么,有什么比让你的 Agent 直接调用 CLI 工具和写代码更简单的呢?这不是什么新鲜事。我们从一开始就一直在这样做。我只是想说服你,在很多情况下,你不需要甚至不想要一个 MCP 服务器。

让我用一个常见的 MCP 服务器用例来说明这一点:浏览器开发者工具。

我的浏览器 DevTools 用例

我的用例是和我的 Agent 一起处理 Web 前端,或者滥用我的 Agent 让它变成一个小小的刮削黑客小子,这样我就可以刮掉世界上所有的数据。对于这两个用例,我只需要一套极简的工具:

  • 启动浏览器,可选使用我的默认配置文件,这样我就已经登录了
  • 导航到一个 URL,在当前标签页或新标签页中
  • 在当前页面上下文中执行 JavaScript
  • 截取视口的屏幕截图

如果我的用例需要额外的特殊工具,我希望能快速让我的 Agent 为我生成并将其与其他工具集成。

Agent 常用浏览器 DevTools 的问题

人们会为我上面说明的用例推荐 Playwright MCPChrome DevTools MCP。两者都很好,但它们需要覆盖所有基础。Playwright MCP 有 21 个工具,使用 13.7k Token(Claude 上下文的 6.8%)。Chrome DevTools MCP 有 26 个工具,使用 18.0k Token(9.0%)。这么多工具会让你的 Agent 感到困惑,特别是与其他 MCP 服务器和内置工具结合时。

使用这些工具也意味着你会遇到可组合性问题:任何输出都必须通过你的 Agent 的上下文。你可以通过使用子 Agent 来某种程度上解决这个问题,但然后你会引入子 Agent 带来的所有问题。

拥抱 Bash(和代码)

这是我的极简工具集,通过 README.md 来说明:

# Browser Tools

用于协作站点探索的极简 CDP 工具。

## Start Chrome

```bash
./start.js              # 全新配置文件
./start.js --profile    # 复制你的配置文件(Cookie、登录信息)
```

:9222 上启动 Chrome 并开启远程调试。

./nav.js https://example.com
./nav.js https://example.com --new

导航当前标签页或打开新标签页。

Evaluate JavaScript

./eval.js 'document.title'
./eval.js 'document.querySelectorAll("a").length'

在活动标签页中执行 JavaScript(异步上下文)。

Screenshot

./screenshot.js

截取当前视口,返回临时文件路径。


这就是我提供给我的 Agent 的全部内容。这是几个覆盖了我的用例所有基础的工具。每个工具都是一个简单的 Node.js 脚本,使用 [Puppeteer Core](https://pptr.dev/)。通过阅读那个 README,Agent 知道可用的工具、何时使用它们,以及如何通过 Bash 使用它们。

当我启动一个 Agent 需要与浏览器交互的会话时,我只是让它完整读取那个文件,这就是它高效工作所需的全部。让我们看看它们的实现,了解这实际上是多么少的代码。

### 启动工具

Agent 需要能够启动一个新的浏览器会话。对于抓取任务,我经常想使用我的实际 Chrome 配置文件,这样我就已经在所有地方登录了。这个脚本要么将我的 Chrome 配置文件 rsync 到一个临时文件夹(Chrome 不允许在默认配置文件上调试),要么全新启动:

```javascript
#!/usr/bin/env node

import { spawn, execSync } from "node:child_process";
import puppeteer from "puppeteer-core";

const useProfile = process.argv[2] === "--profile";

if (process.argv[2] && process.argv[2] !== "--profile") {
	console.log("Usage: start.ts [--profile]");
	console.log("\nOptions:");
	console.log("  --profile  Copy your default Chrome profile (cookies, logins)");
	console.log("\nExamples:");
	console.log("  start.ts            # Start with fresh profile");
	console.log("  start.ts --profile  # Start with your Chrome profile");
	process.exit(1);
}

// 杀死现有的 Chrome
try {
	execSync("killall 'Google Chrome'", { stdio: "ignore" });
} catch {}

// 等待进程完全结束
await new Promise((r) => setTimeout(r, 1000));

// 设置配置文件目录
execSync("mkdir -p ~/.cache/scraping", { stdio: "ignore" });

if (useProfile) {
	// 使用 rsync 同步配置文件(后续运行更快)
	execSync(
		'rsync -a --delete "/Users/badlogic/Library/Application Support/Google/Chrome/" ~/.cache/scraping/',
		{ stdio: "pipe" },
	);
}

// 在后台启动 Chrome(分离模式,以便 Node 可以退出)
spawn(
	"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
	["--remote-debugging-port=9222", `--user-data-dir=${process.env["HOME"]}/.cache/scraping`],
	{ detached: true, stdio: "ignore" },
).unref();

// 尝试连接,等待 Chrome 准备就绪
let connected = false;
for (let i = 0; i < 30; i++) {
	try {
		const browser = await puppeteer.connect({
			browserURL: "http://localhost:9222",
			defaultViewport: null,
		});
		await browser.disconnect();
		connected = true;
		break;
	} catch {
		await new Promise((r) => setTimeout(r, 500));
	}
}

if (!connected) {
	console.error("✗ Failed to connect to Chrome");
	process.exit(1);
}

console.log(`✓ Chrome started on :9222${useProfile ? " with your profile" : ""}`);

Agent 只需要知道使用 Bash 来运行 start.js 脚本,要么带 --profile,要么不带。

导航工具

一旦浏览器运行,Agent 需要导航到 URL,可以在新标签页或活动标签页中。这正是导航工具提供的:

#!/usr/bin/env node

import puppeteer from "puppeteer-core";

const url = process.argv[2];
const newTab = process.argv[3] === "--new";

if (!url) {
  console.log("Usage: nav.js <url> [--new]");
  console.log("\nExamples:");
  console.log("  nav.js https://example.com       # Navigate current tab");
  console.log("  nav.js https://example.com --new # Open in new tab");
  process.exit(1);
}

const b = await puppeteer.connect({
  browserURL: "http://localhost:9222",
  defaultViewport: null,
});

if (newTab) {
  const p = await b.newPage();
  await p.goto(url, { waitUntil: "domcontentloaded" });
  console.log("✓ Opened:", url);
} else {
  const p = (await b.pages()).at(-1);
  await p.goto(url, { waitUntil: "domcontentloaded" });
  console.log("✓ Navigated to:", url);
}

await b.disconnect();

执行 JavaScript 工具

Agent 需要执行 JavaScript 来读取和修改活动标签页的 DOM。它写的 JavaScript 在页面上下文中运行,所以它不需要和 Puppeteer 本身打交道。它只需要知道如何使用 DOM API 写代码,而它当然知道如何做到:

#!/usr/bin/env node

import puppeteer from "puppeteer-core";

const code = process.argv.slice(2).join(" ");
if (!code) {
	console.log("Usage: eval.js 'code'");
	console.log("\nExamples:");
	console.log('  eval.js "document.title"');
	console.log('  eval.js "document.querySelectorAll(\\'a\\').length"');
	process.exit(1);
}

const b = await puppeteer.connect({
	browserURL: "http://localhost:9222",
	defaultViewport: null,
});

const p = (await b.pages()).at(-1);

if (!p) {
	console.error("✗ No active tab found");
	process.exit(1);
}

const result = await p.evaluate((c) => {
	const AsyncFunction = (async () => {}).constructor;
	return new AsyncFunction(`return (${c})`)();
}, code);

if (Array.isArray(result)) {
	for (let i = 0; i < result.length; i++) {
		if (i > 0) console.log("");
		for (const [key, value] of Object.entries(result[i])) {
			console.log(`${key}: ${value}`);
		}
	}
} else if (typeof result === "object" && result !== null) {
	for (const [key, value] of Object.entries(result)) {
		console.log(`${key}: ${value}`);
	}
} else {
	console.log(result);
}

await b.disconnect();

截图工具

有时 Agent 需要对页面有一个视觉印象,所以我们自然想要一个截图工具:

#!/usr/bin/env node

import { tmpdir } from "node:os";
import { join } from "node:path";
import puppeteer from "puppeteer-core";

const b = await puppeteer.connect({
  browserURL: "http://localhost:9222",
  defaultViewport: null,
});

const p = (await b.pages()).at(-1);

if (!p) {
  console.error("✗ No active tab found");
  process.exit(1);
}

const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const filepath = join(tmpdir(), filename);

await p.screenshot({ path: filepath });

console.log(filepath);

await b.disconnect();

这将截取活动标签页当前视口的截图,将其写入临时目录中的 .png 文件,并将文件路径输出给 Agent,然后 Agent 可以反过来读取它并使用其视觉能力来”看到”图像。

好处

那么与我上面提到的 MCP 服务器相比,这有什么优势呢?首先,我可以在需要时引入 README,而不是在每个会话中都为它付出代价。这与 Anthropic 最近引入的技能功能非常相似。除了它更加临时化,并且可以与任何编码 Agent 一起工作。我需要做的就是指示我的 Agent 读取 README 文件。

旁注:在 Anthropic 发布他们的技能系统之前,包括我自己在内的很多人都已经使用过这种设置。你可以在我的”Prompts are Code”博客文章或我的小 sitegeist.ai 中看到类似的东西。Armin 之前也谈到过 Bash 和代码相比 MCP 的力量。Anthropic 的技能添加了渐进式披露(喜欢),并且它们让非技术受众可以跨几乎所有产品使用它们(也喜欢)。

说到 README,与上面提到的 MCP 服务器的 13000 到 18000 Token 相比,这个 README 有惊人的 225 Token。这种效率来自于模型知道如何写代码和使用 Bash。我通过严重依赖它们现有的知识来节省上下文空间。

这些简单工具也是可组合的。Agent 可以决定将它们保存到文件以供以后处理,而不是将调用的输出读入上下文,可以是它自己或代码处理。Agent 还可以在单个 Bash 命令中轻松链接多个调用。

如果我发现工具的输出 Token 效率不高,我可以直接更改输出格式。这是很难或不可能做到的,取决于你使用什么 MCP 服务器。

而且为我的需求添加新工具或修改现有工具 ridiculously 容易。让我来说明。

添加选择工具

当 Agent 和我试图为特定站点想出一个抓取方法时,如果我能够直接通过点击它们来向它指出 DOM 元素,通常会更高效。为了让这变得超级容易,我可以直接构建一个选择器。这是我添加到 README 中的内容:

## Pick Elements

```bash
./pick.js "Click the submit button"
```

交互式元素选择器。点击选择,Cmd/Ctrl+点击多选,Enter 完成。


这是代码:

(由于篇幅原因,代码部分在此省略,与原文一致)

每当我认为我直接点击一堆 DOM 元素比让 Agent 弄清楚 DOM 结构更快时,我可以告诉它使用选择工具。它超级高效,并允许我立即构建抓取器。如果站点的 DOM 布局发生变化,调整抓取器也很棒。

如果你难以理解这个工具的作用,别担心,我会在博客文章的结尾放一个视频,你可以看到它的实际操作。在我们看那个之前,让我向你展示一个额外的工具。

## 添加 Cookie 工具

在我最近的一次抓取冒险中,我需要那个站点的 HTTP-only Cookie,这样确定性抓取器就可以假装是我。执行 JavaScript 工具无法处理这个,因为它在页面上下文中执行。但我告诉 Claude 创建那个工具、将其添加到 readme 并开始工作,花了不到一分钟。

![扩展](../../assets/images/no-mcp-extension.webp)

这比调整、测试和调试现有 MCP 服务器容易得多。

## 一个人为的例子

让我用一个人为的例子来说明这套工具的用法。我着手构建一个简单的 Hacker News 抓取器,我基本上为 Agent 选择 DOM 元素,基于这些元素,它然后可以编写一个极简的 Node.js 抓取器。这就是实际操作的样子。我加速了几个 Claude 平时很慢的部分。

真实世界的抓取任务看起来会稍微复杂一些。而且,对于像 Hacker News 这样简单的站点,这样做也没有意义。但你懂我的意思。

最终 Token 统计:

![Token](../../assets/images/no-mcp-tokens.webp)

## 让它跨 Agent 可重用

这是我如何设置的,以便我可以与 Claude Code 和其他 Agent 一起使用它。我在我的主目录中有一个文件夹 `agent-tools`。然后我将各个工具的仓库,比如上面的浏览器工具仓库,克隆到该文件夹中。然后我设置一个别名:

```bash
alias cl="PATH=$PATH:/Users/badlogic/agent-tools/browser-tools:<other-tool-dirs> && claude --dangerously-skip-permissions"

这样所有脚本都可用于 Claude 的会话,但不会污染我的正常环境。我还为每个脚本添加完整的工具名称前缀,例如 browser-tools-start.js,以消除名称冲突。我还在 README 中添加了一句话告诉 Agent 所有脚本都全局可用。这样,Agent 不必仅仅为了调用工具脚本而更改其工作目录,在这里和那里节省了一些 Token,并减少 Agent 被不断的工作目录更改弄糊涂的机会。

最后,我通过 /add-dir 将 Agent 工具目录作为工作目录添加到 Claude Code,这样我就可以使用 @README.md 来引用特定工具的 README 文件并使其进入 Agent 的上下文。我更喜欢这一点而不是 Anthropic 的技能自动发现,我发现它在实践中不能可靠地工作。这也意味着我又节省了几个 Token:Claude Code 将它能找到的所有技能的所有 frontmatter 注入到系统提示词(或第一个用户消息,我忘了,见 https://cchistory.mariozechner.at

总结

构建这些工具 ridiculously 容易,给你所需的所有自由,并让你、你的 Agent 和你的 Token 使用高效。你可以在 GitHub 上找到浏览器工具。

这个一般原则可以应用于任何有某种代码执行环境的框架。跳出 MCP 框框思考,你会发现这比你必须遵循 MCP 的更严格结构强大得多。

然而,伴随着强大的力量而来的是巨大的责任。你必须自己想出一个如何构建和维护这些工具的结构。Anthropic 的技能系统可能是做到这一点的一种方法,尽管那对其他 Agent 来说不太可转移。或者你遵循我上面的设置。


编辑此页