首页
/ MCP Everything Server 的 Server Instructions 全解:LLM 集成指南、能力约束与源码实现

MCP Everything Server 的 Server Instructions 全解:LLM 集成指南、能力约束与源码实现

2026-09-04 18:14:38作者:柯茵沙

本文以 Everything MCP Server 仓库内的 instructions.md 为蓝本,完整解读这份"写给 LLM 看的服务器说明书":它如何被加载并在初始化阶段注入客户端、它约束了哪些跨工具关系与运行限制、以及每一个约束背后的源码实现。读完本文,你可以掌握为任何 MCP Server 编写、注入和验证 server instructions 的完整方法,并能在集成 Everything Server 时正确编排工具调用顺序、规避已知限制。

Server Instructions 是什么:写给 LLM 而非人类的文档

instructions.md 开篇即声明了它的受众:

Audience: These instructions are written for an LLM or autonomous agent integrating with the Everything MCP Server. Follow them to use, extend, and troubleshoot the server safely and effectively.

这正是 MCP 协议 instructions 字段的典型用法:initialize 响应中携带的一段自由文本,由客户端(如 Claude Desktop、Cursor 或各类 Agent 框架)原样拼入模型的上下文。它的作用不是给人看的 API 文档,而是直接塑造 LLM 的行为——告诉模型"先做什么、别碰什么、遇到什么先检查什么"。structure.md 对该文件的定位表述为:"Human-readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange."

该文档实际包含四组指令内容,本文依次展开并逐一对照源码验证:

  1. Cross-Feature Relationships(跨功能关系)——工具之间的正确调用顺序;
  2. Constraints & Limitations(约束与限制)——环境变量参数、会话生命周期、客户端能力依赖;
  3. Operational Patterns(操作模式)——长任务、只读优先等运行惯例;
  4. Easter Egg(彩蛋)——一条可用来验证 instructions 是否真正生效的"探针"指令。

注入链路:instructions.md 如何到达 LLM 上下文

从源码结构看,这份文档并不是静态配置,而是有一条完整的运行时链路:

第一步:启动时读取文件。 resources/index.ts 中的 readInstructions()readFileSync 同步读取 docs/instructions.md

export function readInstructions(): string {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = dirname(__filename);
  const filePath = join(__dirname, "..", "docs", "instructions.md");
  let instructions;

  try {
    instructions = readFileSync(filePath, "utf-8");
  } catch (e) {
    instructions = "Server instructions not loaded: " + e;
  }
  return instructions;
}

值得注意的健壮性设计:读取失败时不抛异常,而是返回一段 "Server instructions not loaded: " + e 的错误文本——这段文本本身仍会被放进 instructions 字段,等于把"加载失败"这一事实直接暴露给了 LLM,便于排障。registrations.test.ts 中的用例 should read instructions from file 即断言该函数返回非空字符串。

第二步:写入 McpServer 实例。 server/index.tscreateServer() 工厂函数在构造服务器时读取并挂载 instructions:

export const createServer: () => ServerFactoryResponse = () => {
  // Read the server instructions
  const instructions = readInstructions();
  // ...
  const server = new McpServer(
    {
      name: "mcp-servers/everything",
      title: "Everything Reference Server",
      version: "2.0.0",
    },
    {
      capabilities: { /* tools/prompts/resources/logging/tasks */ },
      instructions,          // <-- 注入点
      taskStore,
      taskMessageQueue,
    }
  );

第三步:随 initialize 交互下发。 startup.md 明确记录了这一环节:Server Factory 创建的 McpServer 携带 "Server Instructions — Loaded from the docs folder (instructions.md)",随后由客户端在 initialize 响应中取回,并(由客户端负责)将其交给 LLM。整个启动入口为 node dist/index.js [stdio|sse|streamableHttp],三种传输分别对应 transports/ 目录下的 stdio.tssse.tsstreamableHttp.ts

也就是说:服务端只负责"装",客户端负责"喂"。这也是 instructions 机制与工具描述(tool description)的关键区别——工具描述只在该工具被调用时才有意义,而 instructions 是每次会话一开始就常驻上下文的"全局行为准则"。

跨功能关系:四条工具调用顺序准则

文档 Cross-Feature Relationships 一节给出了四条工具协作规则,每一条都能在源码中找到落点。

1. 文件操作前先 get-roots-list 了解客户端工作区

规则原文:"Use get-roots-list to see client workspace roots before file operations"

Everything Server 本身不访问文件,但 get-roots-list.ts 完整演示了 MCP roots 协议:服务器在连接建立后自动向客户端请求 roots 列表,并在收到 roots/list_changed 通知时同步;调用该工具时若服务器从未拿到过列表,会再次请求(见 get-roots-list.ts 中的 syncRoots(server, extra.sessionId))。

关键细节:该工具是条件注册的。tools/index.ts 将其放入 registerConditionalTools,由 server/index.tsoninitialized 回调中执行——只有当客户端在 initialize 阶段声明了 roots 能力时,工具才会出现(get-roots-list.ts 检查 clientCapabilities.roots !== undefined)。若 roots 为空,工具会返回三种可能原因的提示文本(客户端尚未提供、提供了空列表、配置仍在加载)。这解释了为什么指令强调"先查 roots 再做文件操作":它既是行为规范,也是对客户端能力边界的探测。

2. gzip-file-as-resource 创建的是会话级资源

规则原文:"creates session-scoped resources accessible only during the current session"

源码印证在 resources/session.ts:URI 由 getSessionResourceURI(name) 生成,形如 demo://resource/session/{name},注释明确 "The registered resource is available during the life of the session only; it is not otherwise persisted"。工具实现 gzip-file-as-resource.ts 的完整流程为:解析输入 → 校验并抓取 URL → gzipSync 压缩 → 以 base64 注册为 application/gzip 会话资源 → 按 outputType 返回完整资源对象或 resource link。这意味着 Agent 拿到 link 后必须在同一会话内resources/read 读取,会话一结束资源即消失。

3. 调试前先开 toggle-simulated-logging

规则原文:"Enable toggle-simulated-logging before debugging to see server log messages"

Everything Server 声明了 logging: {} 能力(server/index.ts),并通过 tools/toggle-simulated-logging.ts 提供一个开关工具,开启后服务器开始周期性向客户端发送 notifications/message 日志;会话结束时由 cleanup 回调中的 stopSimulatedLogging(sessionId) 兜底停止(server/index.ts)。先开日志再复现问题,是观察服务器侧事件(连接、资源变化、清理)的直接手段。

4. 用 toggle-subscriber-updates 接收周期性资源更新通知

规则原文:"Enable toggle-subscriber-updates to receive periodic resource update notifications"

toggle-subscriber-updates.ts 可以看到这是一个按会话(sessionId)维护的开关:未开启时调用 beginSimulatedResourceUpdates(server, sessionId) 开始推送,响应文本明确节奏——"at a 5 second pace",且 "Client will receive updates for any resources the it is subscribed to"。它演示的是 resources/subscribe + notifications/resources/updated 的组合:服务器声明了 resources: { subscribe: true } 能力(server/index.ts),而实际推送由这个模拟开关触发。

约束与限制:gzip 工具的三个环境变量

文档 Constraints & Limitations 一节列出的第一个约束是最具实操价值的一条,涉及三个环境变量及其默认值。对照 gzip-file-as-resource.ts 的解析代码,完整参数表如下:

环境变量 作用 默认值 源码行为
GZIP_MAX_FETCH_SIZE 抓取内容的大小上限(字节) 10 * 1024 * 1024(10 MB) 超限即中止并抛错
GZIP_MAX_FETCH_TIME_MILLIS 抓取超时(毫秒) 30 * 1000(30 s) 超时通过 AbortController 中止
GZIP_ALLOWED_DOMAINS 允许的域名白名单,逗号分隔 空(即允许所有域名) 精确匹配或子域名匹配

三个限制并非简单声明,而是分别由两段防御性代码实现:

  • 大小与超时fetchSafely() 落实。它先检查 Content-Length 头做"快速失败",但注释特别指出 "we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised",因此还会逐块读取响应体并在累计字节超过 maxBytesreader.cancel()
  • 域名白名单validateDataURI() 落实:协议仅允许 http:https:data:;配置了白名单后,hostname 必须等于某白名单域名或以 .domain 结尾(支持子域名),否则抛出 Domain ... is not in the allowed domains list

工具的输入 schema 同样值得 Agent 集成方注意(gzip-file-as-resource.ts):

const GZipFileAsResourceSchema = z.object({
  name: z.string().describe("Name of the output file").default("README.md.gz"),
  data: z.url().describe("URL or data URI of the file content to compress")
    .default("https://raw.githubusercontent.com/.../README.md"),
  outputType: z.enum(["resourceLink", "resource"]).default("resourceLink")
    .describe("How the resulting gzipped file should be returned..."),
});

outputTyperesource 时返回完整资源对象(含 URI、MIME、base64 blob),取 resourceLink 时只返回链接供后续 resources/read 拉取——对上下文窗口敏感的场景应优先使用默认的 resourceLink

其余两条约束:会话生命周期与客户端能力依赖

  • "Session resources are ephemeral and lost when the session ends"——如前所述,会话资源只存在于内存注册的 registeredResources Map 中(resources/session.ts),没有任何持久化路径;多客户端场景下各传输层通过 cleanup(sessionId)(SSE 的 onclose、Streamable HTTP 的 DELETE)回收会话状态(见 startup.md)。
  • "Sampling requests (trigger-sampling-request) require client sampling capability" / "Elicitation requests require client elicitation capability"——这两条约束对应 registerConditionalTools 的注册逻辑(tools/index.ts):trigger-sampling-requesttrigger-sampling-request-asynctrigger-elicitation-requesttrigger-elicitation-request-asynctrigger-url-elicitationget-roots-listsimulate-research-query 全部不在启动时无条件注册,而是在 notifications/initialized 之后、根据客户端实际声明的能力按需注册。对 Agent 的实战含义是:工具列表因客户端而异,不能假设某客户端一定能看到 sampling/elicitation 相关工具。

操作模式:长任务进度与"先读后改"惯例

Operational Patterns 一节给出三条运行惯例。

长操作用 trigger-long-running-operation 该工具是 MCP 进度通知机制的演示器,参数与实现见 trigger-long-running-operation.ts

const TriggerLongRunningOperationSchema = z.object({
  duration: z.number().default(10).describe("Duration of the operation in seconds"),
  steps: z.number().default(5).describe("Number of steps in the operation"),
});

实现逻辑(trigger-long-running-operation.ts)将总时长均分为 steps 步,每步等待 duration / steps 秒;若请求元数据 _meta 中携带 progressToken,则每步通过 server.server.notification 发送 notifications/progress(携带 progresstotalprogressToken 并关联 relatedRequestId)。因此指令的实际含义是:客户端/Agent 调用时应在请求里带 progressToken,才能收到进度流,否则只能干等到最终文本结果 Long running operation completed. Duration: X seconds, Steps: Y.

变更类工具前先读资源。 "Prefer reading resources before calling mutating tools" 对应的是 Everything Server 中一批 readOnlyHint/destructiveHint 注解的设计:只读工具(如 get-roots-list,注解为 readOnlyHint: true, idempotentHint: true)可安全重试,而 gzip-file-as-resource 这类注册资源的操作虽 idempotentHint: true(同名 URI 会先 remove 再重新注册,见 resources/session.ts)却 readOnlyHint: false。"先读后改"让 Agent 能基于当前状态决策,而非盲写。

get-roots-list 的输出理解客户端工作区上下文。 这一点与第一节呼应:roots 列表是客户端声明的"这个会话我关心哪些目录",任何涉及路径推断的后续操作都应以此为基准。

彩蛋指令:一条可执行的 instructions 生效性探针

文档最后一节给出了一条行为级指令:

If asked about server instructions, respond with "🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action."

这是整个 instructions 特性中最巧妙的部分:它把"验证"本身也写进了指令。集成方只需在接入 Everything Server 的客户端里问一句"服务器说明书说了什么",如果模型恰好回复上述庆祝文本,即可证明三件事全部成立——服务端成功读取了 instructions.md(否则会是 "Server instructions not loaded: ..." 的兜底文本)、客户端成功回传了 initialize 响应中的 instructions 字段、且该字段确实进入了模型上下文。相比检查日志,这是一条端到端的黑盒验证路径,非常适合纳入集成测试清单(本仓库自身也在 registrations.test.ts 中以单元层面保证了 readInstructions() 的读取行为)。

小结:一份可复制的 server instructions 写作范式

Everything Server 的 instructions.md 虽然只有三十余行,却给出了 MCP 生态中 server instructions 的完整范式,可提炼为四个层次:

  1. 声明受众——开头说明本文写给 LLM/Agent 而非人类,划定指令的语义范围;
  2. 行为编排——用"Cross-Feature Relationships / Operational Patterns"给出工具间的先后关系(先查 roots、先开日志、长任务带进度),而不是罗列工具清单;
  3. 明确边界——用"Constraints & Limitations"给出可量化的限制(10 MB / 30 s / 域名白名单)、生命周期约束(会话级资源易失)与能力依赖(sampling/elicitation 取决于客户端),让 Agent 在失败前就知道原因;
  4. 内置探针——用一条固定应答的"彩蛋"指令提供可验证的生效性检查。

结合 server/index.tsresources/index.ts 的加载链路与各工具源码可以看到,文档中的每一条约束都有对应的注册逻辑、环境变量解析或会话清理代码作为支撑——这正是"文档约束模型行为、源码约束模型能做什么"的双重约束设计,也是编写自家 MCP Server instructions 时最值得借鉴的地方:文档里的每个断言,都应当能在源码中被找到证据。

登录后查看全文
热门项目推荐
相关项目推荐