@reinsjs/tools-mcp 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +62 -1
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @reinsjs/tools-mcp
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Version alignment: every `@reinsjs/*` package ships as 0.3.0 together with `@reinsjs/core` and `@reinsjs/brain` (MCP tools on the `lazyTools()` menu via `SocketSetup.tools`). No behaviour change in these packages beyond the dependency bump.
8
+
9
+ ### Patch Changes
10
+
11
+ - a3afbf9: README: a recipe for large flat tool tables (TikTok's 377-tool endpoint) — `override` keeps only `readOnlyHint` tools and marks them `lazy: true` for `@reinsjs/brain`'s `lazyTools()`, plus a `withBusinessErrors` wrapper that turns `{"code": <non-zero>}` envelopes returned as successful results into `isError` results. Executable version in `src/readonly-lazy.recipe.test.ts`. No code changes.
12
+ - Updated dependencies [a3afbf9]
13
+ - @reinsjs/core@0.3.0
14
+
3
15
  ## 0.2.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -179,7 +179,68 @@ Two more things worth knowing about servers of this shape:
179
179
  budget for that one tool: `override: (tool, info) => info.name === "tool_list" ? { ...tool, resultPolicy: { maxTokens: 100_000, overflow: "spill" } } : tool`.
180
180
  - **Upstream business errors may not set `isError`.** TikTok returns `{"code": 40001, "message": "..."}` as a
181
181
  *successful* MCP result; only MCP-level failures (unknown tool, transport error) are `isError`. Anything keying
182
- off `isError` — retries, your own policies — will not see those. The model reads the code from the JSON.
182
+ off `isError` — retries, your own policies, `onEvent` observers — will not see those unless you translate them;
183
+ the `withBusinessErrors` wrapper in the next section does exactly that.
184
+
185
+ ## Large flat tool tables
186
+
187
+ The opposite shape: hundreds of ordinary tools, each annotated honestly. TikTok's flat endpoint lists 377 of them,
188
+ about 200 reads and 130 writes, every one with `readOnlyHint`. Two things you usually want there: keep the writes
189
+ out of the model's reach entirely (they go through your own tools, with your own authorization and audit trail),
190
+ and stop sending two hundred tool definitions on every request. Both are one `override` plus
191
+ `@reinsjs/brain`'s `lazyTools()`:
192
+
193
+ ```ts
194
+ import { lazyTools } from "@reinsjs/brain"
195
+ import { normalizeToolOutput, type Tool, type ToolResult } from "@reinsjs/core"
196
+
197
+ /** TikTok puts business errors in a successful result: text = {"code": <non-zero>, "message": ...} */
198
+ const businessErrorOf = (result: ToolResult): string | undefined => {
199
+ if (result.isError) return undefined
200
+ for (const part of result.content) {
201
+ if (part.type !== "text") continue
202
+ try {
203
+ const body = JSON.parse(part.text) as { code?: unknown; message?: unknown }
204
+ if (typeof body.code === "number" && body.code !== 0)
205
+ return `Upstream error ${body.code}: ${String(body.message ?? "")}`
206
+ } catch {
207
+ // not JSON, not an error envelope
208
+ }
209
+ }
210
+ return undefined
211
+ }
212
+
213
+ /** Keep the original text for the model, but mark it as an error so retries, policies and observers see it */
214
+ const withBusinessErrors = (tool: Tool): Tool => ({
215
+ ...tool,
216
+ async execute(input, ctx) {
217
+ const result = normalizeToolOutput(await tool.execute?.(input, ctx))
218
+ const error = businessErrorOf(result)
219
+ return error ? { content: [...result.content, { type: "text", text: error }], isError: true } : result
220
+ },
221
+ })
222
+
223
+ /** reads: on the menu, loaded on demand; everything else: not a tool the model has */
224
+ const readOnlyLazy = (tool: Tool, info: McpToolInfo) =>
225
+ info.annotations?.readOnlyHint === true ? withBusinessErrors({ ...tool, lazy: true }) : false
226
+
227
+ const tiktok = mcpTools({ transport, override: readOnlyLazy })
228
+
229
+ createAgent({ sockets: [tiktok, lazyTools()] }) // order matters — see below
230
+ ```
231
+
232
+ `lazyTools()` builds its menu from the tools bound *before* it: the host's, plus those of every socket registered
233
+ earlier. So it goes after `mcpTools()`. The other order is not an error, just useless: the MCP tools are sent in
234
+ full every turn, and `lazyTools()` warns once, naming them. With a lowering layer that has a native place for
235
+ deferred tools (`@reinsjs/lowering-fetch` on Anthropic) loading a tool does not even re-warm the prompt cache; see
236
+ the brain README for the numbers.
237
+
238
+ Loaded definitions are trusted like the tools block itself (`tool_find` results are `trust: "system"`) — the
239
+ same descriptions were already going to the model unwrapped. A 200-line menu costs roughly 10k tokens of system
240
+ prompt; it sits in the cached prefix and does not change within a run, and `lazyTools({ summarize })` can shorten it.
241
+
242
+ The executable version of this recipe is `packages/tools-mcp/src/readonly-lazy.recipe.test.ts`, including the
243
+ wrong-order case.
183
244
 
184
245
  ## Verified upstream behaviour
185
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reinsjs/tools-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "reins — MCP servers as a Socket: tools/list once per run, tools/call as Tool.execute, annotations → risk / approval defaults. Streamable HTTP on the Web-standard entry, stdio under ./node",
5
5
  "keywords": [
6
6
  "agent",
@@ -37,13 +37,13 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/client": "2.0.0",
40
- "@reinsjs/core": "0.2.1"
40
+ "@reinsjs/core": "0.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@modelcontextprotocol/server": "2.0.0",
44
44
  "@types/node": "^22.0.0",
45
45
  "zod": "^4.2.0",
46
- "@reinsjs/brain": "0.2.1"
46
+ "@reinsjs/brain": "0.3.0"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22"