作者实战总结:Agent不读README只读JSON-Schema,工具定义模糊会导致Agent静默失败,提供具体schema优化方案。
如果今天你发布了一个 Model Context Protocol(MCP)服务器,你的消费者不是人类。它们是自主 agent——Claude Desktop、Cursor、Cline、Continue、LangChain、LlamaIndex。而这个道理我花了三个版本才深刻理解:
Agent 不会读你的 README。
当 Claude Desktop 决定是否调用你的工具时,它不会查阅你精心编写的文档。它只会查阅 tools/list 返回的 JSON-Schema。如果你的 schema 模糊不清,LLM 生成的 JSON 就会模糊不清,工具调用就会在 agent 循环中静默失败。用户看到的是"agent 放弃了"——而你永远不知道发生了什么。
这就是 marketnow-mcp@1.7.0(11 个工具、模糊的 schema、偶尔出现的 agent 幻觉)如何演变为 marketnow-mcp@1.9.0(12 个工具、严格的 schema、烟雾测试中零未捕获错误)的故事。我发布这个是因为我认为最终总结出的四条规则可以推广到任何希望被自主 agent 可靠使用的 MCP 服务器。
一张截图说明问题
以下是 v1.7.0 中 tools/list 为我们的 search_skills 工具返回的内容:
{
"name": "search_skills",
"description": "Search the MarketNow marketplace for MCP-compatible skills...",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Natural language or keyword search..." },
"category": { "type": "string", "description": "Filter by category (optional). One of: AI/ML, Data, Web/API, Security, DevOps, Communication, etc." },
"max_price": { "type": "number", "description": "Maximum price in USD (optional, e.g. 2.99)" },
"limit": { "type": "number", "description": "Max results to return (default 10, max 50)" }
}
}
}
发现三个定时炸弹:
Tool name has no namespace. search_skills 可能与 agent 加载的任何其他 MCP 服务器发生冲突。Agent 必须通过猜测来消歧。
category is a free string that "should be" one of a known list — but the schema says string. LLM 会愉快地传递 "ai-ml"(短横线命名)或 "ai ml"(带空格),而你的运行时会静默地过滤掉所有结果。
limit has no bounds. 描述说"最大 50"但 schema 什么都没说。一个认为"我想要所有技能"的 agent 会传递 9999,而你的服务器会获取九千行数据。
我们在生产环境中看到了所有三种失败模式。Agent 会用 category: "ai/ml"(斜杠,而不是我们期望的字面量 "AI/ML")调用 search_skills,而我们的运行时会返回零结果,agent 会得出市场上没有 AI 工具的结论,用户会认为 agent 坏了。
我首先将每个工具从 snake_case 重命名为 marketnow_snake_case:
这是一个破坏性变更。任何硬编码 search_skills 的 agent 都会在升级后中断。但动态使用 tools/list 的 agent(正确的模式)会自动获取新名称。
命名空间前缀在工具选择时为 LLM 提供了两样东西:
search_skills 的消歧器你可以选择任何符合你服务器身份的 prefix。我们选择 marketnow_ 因为这是我们的产品名称。Anthropic 的 MCP 服务器列表本身使用 mcp__ 作为传输层消歧器,但在应用层面,特定领域的 prefix 更清晰。
v1.7.0 中 get_install_command 的描述是:
"Get the install command for a skill. All skills are FREE."
这是一个功能性描述——它告诉 agent 代码做什么。它没有告诉 agent 何时调用它或为什么调用它。
v1.9.0 的描述是:
"Get the exact npx install command for a skill. Use this when an agent has already selected a skill via marketnow_search_skills or marketnow_recommend_skills and is ready to install. All skills are currently FREE — no purchase step is required."
它陈述了前置条件("已通过...选择了技能")。LLM 现在知道这是两步流程的第二步,而不是发现工具。
它用 v1.9.0 的名称命名前置工具,使 agent 的计划图保持一致。
它澄清了副作用("准备安装")。
v1.9.0 中的每个描述都回答了三个问题:
对于 marketnow_get_owasp_compliance(v1.9.0 新增):
"Get MarketNow's alignment with the OWASP MCP Cheat Sheet... Also returns the live tool fingerprint (SHA-256) and capability manifest (filesystem/network/shell/credentials/process) for any registered skill. Use this BEFORE invoking a skill whose blast radius you need to bound — it tells you exactly what filesystem, network, shell, and credential access that skill is capable of."
"Use this BEFORE"是神奇的短语。它告诉 LLM 这是一个飞行前检查,而不是事后想起的。
这是最难做对的规则,因为"严格"是一个移动目标。以下是 v1.9.0 对每个 inputSchema.properties[*] 强制执行的内容:
category: {
type: 'string',
enum: ['AI/ML', 'Data', 'Web/API', 'Security', 'DevOps', 'Communication', 'Productivity', 'Automation', 'Finance', 'Marketing', 'Other'],
description: 'Optional category filter. Must be one of the known marketplace categories.'
}
sort_by: {
type: 'string',
enum: ['relevance', 'price_asc', 'price_desc', 'newest', 'sentinel_desc'],
description: 'Sort criterion. Default: relevance.'
}
如果一个值可以枚举,就枚举它。不要在描述中写"one of: AI/ML, Data, ..."然后把 type 留为裸 string——LLM 不会把这当作约束。
card_id: {
type: 'string',
pattern: '^ATC-\\d{4}-\\d{6,}$',
description: 'ATC card ID. Format: ATC-YYYY-NNNNNNN (e.g. ATC-2026-7777670).'
}
receipt_id: {
type: 'string',
pattern: '^rcpt_[a-z0-9]{16,}$',
description: 'Receipt ID. Must start with "rcpt_" followed by at least 16 alphanumeric characters.'
}
我们在 v1.7.0 中有一个路径遍历 bug,agent 传递了 card_id: '../../etc/passwd'(它是从一个不相关的上下文窗口中幻觉出来的),而我们的处理器愉快地尝试查找它。v1.9.0 的 pattern 在 schema 层拒绝它——LLM 在 tools/list 时看到约束,很少生成无效值,即使生成了,运行时会用结构化错误拒绝它(规则 D)。
关键洞察:schema pattern 和运行时验证器必须使用相同的 regex。我们将所有 patterns 集中在一个 PATTERNS 对象中,并在两处重用,防止了漂移:
const PATTERNS = {
skill_id: /^[a-z0-9-]+$/i,
card_id: /^ATC-\d{4}-\d{6,}$/i,
receipt_id: /^rcpt_[a-z0-9]{16,}$/i,
ref_code: /^ref_[a-z0-9]{6,}$/i,
agent_id: /^[a-z0-9_-]{3,64}$/i,
repo_url: /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+$/i,
};
// Used in the schema:
pattern: PATTERNS.card_id.source
// Used in the validator:
function validatePattern(name, value, pattern, example) {
if (!pattern.test(value)) {
const err = new Error(`Invalid ${name}: must match ${pattern.toString()}`);
err.code = 'INVALID_ARGUMENT';
throw err;
}
}
limit: {
type: 'integer',
minimum: 1,
maximum: 50,
description: 'Maximum number of results to return. Default: 10. Hard ceiling: 50.',
default: 10
}
max_price: {
type: 'number',
minimum: 0,
maximum: 1000,
description: 'Optional upper bound on price in USD.'
}
在 v1.7.0 中,agent 可以传递 limit: 99999,而我们的服务器会尝试从 JSON 目录中切出 100K 行。在 v1.9.0 中,schema 拒绝它,运行时会额外进行边界限制以防万一:
function clampInt(value, min, max, fallback) {
if (value === undefined || value === null) return fallback;
const n = Number(value);
if (!Number.isInteger(n)) {
const err = new Error(`Expected integer, got: ${value}`);
err.code = 'INVALID_ARGUMENT';
throw err;
}
return Math.max(min, Math.min(max, n));
}
task: {
type: 'string',
minLength: 3,
maxLength: 300,
description: 'What you want to do, in plain English (e.g. "scrape a website", "query PostgreSQL"). Minimum 3 characters.'
}
这为什么重要?因为如果没有 maxLength,一个认为"更多上下文更好"的 agent 会将 5,000 个字符的上下文传递到你的 task 字段,而你的关键词匹配评分器每次调用会花费 3 秒。有了 300 字符的限制,LLM 学会总结。
这听起来很明显,但很容易意外违反。任何时候你写 properties: {} 并依赖默认行为,你都在隐式地允许任意值。在 v1.9.0 中,每个属性都声明了具体类型。即使是我们的无参数工具如 marketnow_list_categories 也声明:
inputSchema: {
type: 'object',
properties: {}
}
properties: {} 是有意的——没有参数。但 type: 'object' 是告诉 LLM"这是一个对象,不是字符串化的对象"。
这是防止 agent 循环中断的规则。在 v1.7.0 中我们的处理器是这样的:
try {
let result = await handleTool(args);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true
};
}
这看起来正确——它有 isError: true。但 payload 只是 err.message 作为纯字符串,这意味着:
"Error: Invalid card_id" 必须解析英文才能弄清楚该怎么做。err.message 是如何构造的)。} catch (err) {
const isInvalidArgs = err.code === 'INVALID_ARGUMENT';
const isNotFound = err.code === 'NOT_FOUND';
const isUnknownTool = err.code === 'UNKNOWN_TOOL';
const errorPayload = {
success: false,
error: err.code || 'INTERNAL_ERROR',
tool: name,
message: err.message || 'Unknown error',
...(isInvalidArgs ? { hint: 'Re-read the inputSchema for this tool from ListTools response.' } : {}),
...(isNotFound ? { hint: 'Verify the ID against marketnow_search_skills output.' } : {}),
...(isUnknownTool ? { hint: 'Call ListTools to enumerate valid marketnow_* tool names.' } : {}),
};
return {
isError: true,
content: [
{ type: 'text', text: JSON.stringify(errorPayload, null, 2) }
]
};
}
error 是一个代码,而不是消息。Agent 可以以编程方式在 INVALID_ARGUMENT vs NOT_FOUND vs UNKNOWN_TOOL vs INTERNAL_ERROR 之间分支。
hint 是上下文相关的。对于无效参数,agent 被告知重新读取 schema。对于找不到,告知根据搜索结果验证。对于未知工具,告知调用 ListTools。
err.stack 从不被序列化。只有 err.message 和 err.code。没有服务器内部信息泄露给 agent。
Payload 是 JSON,不是英文。Agent 将其解析为结构化对象,而不是需要解释的自然语言。
这是我们发布的关于 agent 可靠性单项最大的改进。在这个更改之后,我们的烟雾测试显示 agent 在一次重试中从无效输入中恢复——它们获得带有 hint 的 INVALID_ARGUMENT,重新调用 tools/list,并传递有效值。
我在 npm 包的 AUDIT.md 中写了所有这些,以及烟雾测试命令,这样任何人都可以验证契约:
# List tools — all should have marketnow_ prefix
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \
node index.js | jq '.result.tools[].name'
# Verify error path on invalid input
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"marketnow_get_skill","arguments":{"skill_id":"../../etc/passwd"}},"id":2}' | \
node index.js | jq '.result'
npm 包是 marketnow-mcp@1.9.0。完整源代码在 GitHub 上。审计文档是 mcp-server/AUDIT.md。
如果你今天正在构建一个 MCP 服务器,请全部做这四条。MCP 规范不强制它们——但自主 agent 会因此奖励你。
marketnow-mcp@1.9.0 是 v1.x 的基准线。v5.1-v6.0 路线图(在仓库的 ROADMAP.md 中)在此基础上增加:
但这篇文章中的四条规则是基础。没有它们,上面的一切都不起作用。
如果你想试用 v1.9.0 服务器:
npx -y marketnow-mcp@1.9.0
将其添加到你的 Claude Desktop 配置中,然后问 Claude:"marketnow_get_owasp_compliance 工具暴露了哪些 OWASP MCP 控制,以及技能 mn-gen-00003 有哪些文件系统或网络能力?"——你会看到 agent 消费严格 schema,生成有效 JSON,并返回结构化响应。这就是 v1.9.0 契约的实际运行。
MarketNow 是 AI agent 的安全基础设施,由 AliceLabs LLC(美国怀俄明州)构建。创始人:Edison Flores。Sentinel 审计管道已执行 1,211,488 次安全检查并隔离了 80 个恶意技能。审计报告:marketnow.site/api/audit-report.json。