fix(mcp): enforce recall limit, align server version, scope the client claim

Three things found by actually running the server over stdio against a live backend
instead of describing it.

1. recall ignored 'limit'. The backend treats it as a retrieval hint and returns
   everything inside its own token budget — 113 facts, 43 KB of JSON, for a request
   that asked for 2. The tool's own description ('Max results') was therefore false,
   and every recall spent roughly 10k tokens of the caller's context. The MCP layer
   now enforces the requested limit and reports 'N of M' when it truncated:
   limit=2 goes from 43541 to 1000 chars.

2. serverInfo.version said 1.0.0 while package.json and server.json say 0.1.0. The
   MCP registry validates the resolved package against the manifest, so a version
   that disagrees with the tarball is a review failure.

3. The README claimed 'Claude Code, OpenClaw, Cursor, etc.' None of those was tested.
   Replaced with what was measured — MCP 2025-06-18, initialize/tools/list/tools/call
   round-trip, memory_recall returning real results over stdio — plus the Claude Code
   config we actually run.
This commit is contained in:
RCLL 2026-08-24 08:46:24 +03:00
parent 45c1a3c633
commit 0af516ae69
2 changed files with 30 additions and 3 deletions

View file

@ -125,7 +125,16 @@ Dimension is detected automatically. ⚠️ Switching models changes the vector
## MCP Server ## MCP Server
The `mcp-server/` directory contains a standalone [MCP](https://modelcontextprotocol.io) server. Any MCP-compatible client (Claude Code, OpenClaw, Cursor, etc.) connects and gets structured long-term memory. The `mcp-server/` directory contains a standalone [MCP](https://modelcontextprotocol.io) server over stdio.
**What is actually verified**, as of 2026-08-24, against a live backend: protocol version
`2025-06-18`; `initialize`, `tools/list` and `tools/call` all round-trip; `memory_recall`
returns real results. That is a protocol-level check run directly over stdio — not a
client-by-client compatibility matrix.
Any client that speaks MCP over stdio should therefore work, but we have not sat in front
of each one. Listed below is the config we run ourselves (Claude Code) and no others. If
you get it working with a different client, a PR to this section is the useful kind.
### Tools ### Tools
@ -139,6 +148,11 @@ The `mcp-server/` directory contains a standalone [MCP](https://modelcontextprot
`memory_recall` is the only one of the five that never calls a model. `memory_reflect` is an agentic loop with repeated LLM calls — if you expose this server to anything untrusted, expose `memory_recall` alone. `memory_recall` is the only one of the five that never calls a model. `memory_reflect` is an agentic loop with repeated LLM calls — if you expose this server to anything untrusted, expose `memory_recall` alone.
One thing worth knowing before you budget context: the backend treats `limit` on recall as
a retrieval hint, not a result cap — it returns everything inside its own token budget,
around 110 facts. The MCP layer enforces your `limit` on the way out, so a `limit: 2` recall
costs about 1 KB instead of 43 KB. Recall spends no model call; it does spend context.
### Setup ### Setup
```bash ```bash

View file

@ -98,7 +98,10 @@ async function hindsightRequest(method, path, body = null) {
const server = new McpServer({ const server = new McpServer({
name: 'rcll', name: 'rcll',
version: '1.0.0', // Keep in step with package.json and server.json — the MCP registry validates
// the package it resolves against this manifest, and a version that disagrees
// with the tarball is a review failure, not a cosmetic drift.
version: '0.1.0',
}); });
// Tool 1: memory_retain // Tool 1: memory_retain
@ -177,7 +180,7 @@ server.tool(
const result = await hindsightRequest('POST', `/${bankId}/memories/recall`, body); const result = await hindsightRequest('POST', `/${bankId}/memories/recall`, body);
const memories = (result.results || []).map(r => ({ const all = (result.results || []).map(r => ({
id: r.id || r.uuid || null, id: r.id || r.uuid || null,
text: r.text, text: r.text,
type: r.type, type: r.type,
@ -187,6 +190,13 @@ server.tool(
hall: r.hall || null, hall: r.hall || null,
})); }));
// The backend treats `limit` as a retrieval hint, not a result cap: it returns
// everything that fits its own token budget (~110 facts, ~40 KB of JSON) no
// matter what we ask for. Unenforced here, the tool's own description would be
// false and every recall would spend ~10k tokens of the caller's context.
const effectiveLimit = body.limit;
const memories = all.slice(0, effectiveLimit);
return { return {
content: [{ content: [{
type: 'text', type: 'text',
@ -194,6 +204,9 @@ server.tool(
success: true, success: true,
bank_id: bankId, bank_id: bankId,
count: memories.length, count: memories.length,
...(all.length > memories.length
? { returned_of_available: `${memories.length} of ${all.length}` }
: {}),
memories, memories,
}, null, 2), }, null, 2),
}], }],