@frontmcp/plugin-result-handles

中文文档
1.12.2发布于 2026/11/15Apache-2.0TypeScriptESM + CJS构建来源已验证近 30 天下载 2513

Result handles plugin for FrontMCP: folds large tool answers into a preview, a shape and a handle, and lets the model read the rest with result_query

原始使用文档
文档目录11 节
  1. Install
  2. Usage
  3. What folds
  4. Tools with an output schema
  5. result_query
  6. Who can read a handle
  7. Options
  8. Docs
  9. Related packages
  10. Contributing
  11. License

@frontmcp/plugin-result-handles

A FrontMCP plugin for tools whose answers are too large to send whole. An answer over a size threshold is kept on the server, and the model gets a preview, a shape and a handle in its place. It then reads only the parts it needs with the result_query tool: a path, a range or a page, and the fields it wants. A handle works only for the session and the caller it was issued to, and only until its TTL runs out.

npm License

Install

npm install @frontmcp/plugin-result-handles @frontmcp/sdk

Requires Node.js 24 or later. Keep @frontmcp/sdk on the same version as this package.

Usage

Register the plugin on the app whose answers it should fold, or on @FrontMcp({ plugins }) for every app. On several apps, the server lists one result_query, and it reads the handles of every app's copy:

import ResultHandlesPlugin from '@frontmcp/plugin-result-handles';
import { App } from '@frontmcp/sdk';

@App({
  id: 'orders',
  name: 'Orders',
  tools: [ExportOrdersTool],
  plugins: [ResultHandlesPlugin.init({ thresholdBytes: 32 * 1024, preview: { items: 2 } })],
})
export class OrdersApp {}

An answer of 500 orders then reaches the client as:

{
  "preview": {
    "orders": [
      { "id": 0, "status": "open" },
      { "id": 1, "status": "shipped" }
    ],
    "total": 500
  },
  "shape": {
    "type": "object",
    "keys": {
      "orders": {
        "type": "array",
        "length": 500,
        "items": {
          "type": "object",
          "keys": { "id": { "type": "number" }, "status": { "type": "string", "length": 4 } }
        }
      },
      "total": { "type": "number" }
    }
  },
  "handle": "3f1c2a4e-9b7d-4c1e-8f2a-1b2c3d4e5f60"
}

as structuredContent and as its text, followed by a line telling the model how to read the rest. The result's _meta['frontmcp/result-handle'] holds { handle, bytes, ttlSeconds }.

What folds

The plugin runs on the fitAnswer stage of tools/call, before the answer budget (output.budget), so the budget sees the folded answer. An answer folds when all of these hold:

  • a client called the tool: a tool calling another with this.callTool() gets the whole answer;
  • the answer is one text content, or structuredContent with at most one text that only repeats it (as the SDK writes it). Images, audio, resources, several texts, a text beside structuredContent that says something else, error results and answers that render a widget are left alone;
  • its data, structuredContent or the text, is larger than thresholdBytes as UTF-8 JSON;
  • the call has a verified session or an authenticated caller. A stateless anonymous call is left alone, since nobody could read its handle back;
  • foldPolicy(toolName, answer, context) returns true. answer is the structuredContent, or the text; a text that is a JSON object or array is parsed, so the policy and result_query get the value;
  • the tool's output schema admits the folded form (below).

When the store can't keep the answer (Redis is down, say), the answer is sent whole and the plugin logs a warning.

Tools with an output schema

A client checks structuredContent against the outputSchema it was listed, so a tool with an output schema folds only when that schema admits { preview, shape, handle }. Add foldedAnswerSchema to it as a union member:

import { foldedAnswerSchema } from '@frontmcp/plugin-result-handles';
import { Tool, ToolContext, z } from '@frontmcp/sdk';

const reportSchema = z.object({ orders: z.array(orderSchema), total: z.number() });

@Tool({ name: 'export_orders', inputSchema: {}, outputSchema: z.union([reportSchema, foldedAnswerSchema]) })
export class ExportOrdersTool extends ToolContext {
  /* … */
}

A Zod schema must accept the folded form with no undeclared key; a JSON Schema (an OpenAPI or remote tool's) must validate it. A tool whose schema doesn't admit it is sent whole, and the plugin logs a warning naming the tool once.

result_query

ArgumentDescription
handleThe handle of the folded answer. Required.
pathA dot path ($.orders[2].note, orders.2.note) or a JSON pointer (/orders/2/note). The whole answer if left out.
offsetOf a list, the first item to read; of a string, the first character.
limitHow many items (20 by default) or characters (2000 by default) to read.
pageA page of limit items, from 1, in place of offset.
fieldsKeep only these keys of the object, or of each object in the list.

It answers { path, value, total?, offset?, nextOffset? }: total is the length of the list or string, and nextOffset is set while there is more to read. Its errors carry a remedy for the model:

CodeWhen
RESULT_HANDLE_NOT_FOUNDThe handle expired, was never issued, or belongs to another session or caller.
RESULT_PATH_NOT_FOUNDThe path isn't a dot path or a JSON pointer, or names nothing in the answer.
RESULT_SLICE_TOO_LARGEThe slice is larger than thresholdBytes: read fewer items, fewer fields or a deeper path.
INVALID_INPUTBoth page and offset were given, or an argument is out of range.

Who can read a handle

A handle is a random UUID, never derived from the answer. It's bound to the call's verified session and, unless the caller is anonymous, to its authenticated subject. A different session or a different caller gets RESULT_HANDLE_NOT_FOUND, the same answer as for a handle that never existed, and never the data.

The store never sees the handle. Each answer is encrypted with AES-256-GCM, and both its encryption key and its storage key are derived (HKDF-SHA256) from the handle, the session and the subject. The storage holds nothing readable without the handle, and a lookup by another session or caller finds no record at all, as for an unknown or expired handle.

Options

OptionDefaultDescription
thresholdBytes16384Answers larger than this fold, and no result_query slice is larger. At least 1024.
ttlSeconds600How long a folded answer can be read.
preview{ items: 5, chars: 500 }The first items of each list and chars of each string the preview keeps.
foldPolicyevery answer(toolName, answer, { bytes, fullName, request }) => boolean: which answers over the threshold fold.
storage{ type: 'auto' }Where folded answers are kept, passed to createStorage() from @frontmcp/utils: memory, Redis, Vercel KV, Upstash or SQLite.
namespace'result-handles'The key prefix of folded answers in the storage.

Invalid options throw a ResultHandlesConfigurationError from init(), naming each problem.

With storage: { type: 'auto' } the store is Redis, Vercel KV or Upstash when their environment variables are set, and memory otherwise. Memory keeps answers in one process, so use a shared store when the server runs on several instances:

ResultHandlesPlugin.init({ storage: { type: 'redis', redis: { url: process.env.REDIS_URL } } });
ResultHandlesPlugin.init({ storage: { type: 'sqlite', sqlite: { path: '/var/lib/my-server/results.sqlite' } } });

SQLite needs @frontmcp/storage-sqlite. In a browser build (create() in a web page, WebMCP), Redis and SQLite throw BrowserStoreUnavailableError from init(), naming storage: { type: 'memory' }: a page has no TCP socket and no file. { type: 'auto' } picks memory there.

Each folded answer is kept, encrypted, until its TTL runs out. A memory store holds 1000 answers at most (DEFAULT_MEMORY_MAX_ENTRIES) and drops the oldest past that; set storage.memory.maxEntries to change it, or to 0 for no limit.

Docs

  • @frontmcp/sdk: the framework this plugin extends, and the answer budget (output.budget) that runs after it
  • @frontmcp/utils: createStorage(), which builds the store

Contributing

Source: plugins/plugin-result-handles. Issues and pull requests are welcome; see CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

版本与发布记录

@frontmcp/plugin-result-handles 共发布 14 个版本,首次发布于 2026/11/04;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.12.2latest2026/11/151 天前
1.12.12026/11/132 天前
1.12.02026/11/114 天前
1.12.0-rc.1rc2026/11/114 天前
1.11.32026/11/106 天前
1.11.22026/11/096 天前

关于 @frontmcp/plugin-result-handles 的常见问题

@frontmcp/plugin-result-handles 是什么?

Result handles plugin for FrontMCP: folds large tool answers into a preview, a shape and a handle, and lets the model read the rest with result_query

如何安装 @frontmcp/plugin-result-handles?

在项目目录运行 npm install @frontmcp/plugin-result-handles,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@frontmcp/sdk(1.12.2)、@frontmcp/lazy-zod(1.12.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@frontmcp/plugin-result-handles 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@frontmcp/plugin-result-handles 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@frontmcp/plugin-result-handles 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@frontmcp/plugin-result-handles 可以商用吗?

@frontmcp/plugin-result-handles 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@frontmcp/plugin-result-handles 的下载量有多少?

根据 npm 官方下载统计,@frontmcp/plugin-result-handles 在 2026/10/16 – 2026/11/14 这 30 天内被下载 2,513 次,日均约 84 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@frontmcp/plugin-result-handles 还在维护吗?

最新版本 1.12.2 发布于 2026/11/15(1 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/11/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。