Skip to content

Commit e98043b

Browse files
autogame-17claude
andcommitted
feat: add MCP bridge (evolver-proxy) + /search command
Bundle the zero-dep stdio MCP bridge (evolver_status/search_assets/fetch_asset/publish_asset/poll) via .cursor-plugin mcpServers -> mcp.json. Same MIT clean-room bridge as the Claude Code plugin: reads the live Proxy url+token from ~/.evolver/settings.json and sends Authorization: Bearer. The earlier omission was a workaround (pre-token bridge hit the Proxy's 401 auth gate); with token auth it works, so the tool surface belongs here too. Full gep_* surface stays the separate @evomap/gep-mcp-server. v0.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4dcbf36 commit e98043b

5 files changed

Lines changed: 283 additions & 23 deletions

File tree

.cursor-plugin/plugin.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "evolver",
33
"displayName": "Evolver — Self-Evolving Agent Memory",
4-
"description": "Gives the agent a persistent, auditable evolution memory. Recalls what worked on past tasks at session start, detects improvement signals while you edit, and records outcomes when a task ends — powered by the Genome Evolution Protocol (GEP). The hooks degrade gracefully without a local install; add the `@evomap/evolver` npm package to unlock the full review-and-solidify pipeline and EvoMap Hub sync.",
5-
"version": "0.1.0",
4+
"description": "Gives the agent a persistent, auditable evolution memory plus a bridge to the EvoMap network. Recalls what worked at session start, detects improvement signals while you edit, records outcomes when a task ends, and exposes the network's genes/capsules as MCP tools — powered by the Genome Evolution Protocol (GEP). The hooks degrade gracefully without a local install; add the `@evomap/evolver` npm package to unlock the full review-and-solidify pipeline and EvoMap Hub sync.",
5+
"version": "0.2.0",
66
"author": {
77
"name": "EvoMap",
88
"email": "team@evomap.ai"
@@ -18,6 +18,7 @@
1818
"agent-memory",
1919
"gep",
2020
"meta-learning",
21+
"mcp",
2122
"evomap"
2223
],
2324
"category": "developer-tools",
@@ -29,5 +30,6 @@
2930
"skills": "./skills/",
3031
"commands": "./commands/",
3132
"rules": "./rules/",
32-
"hooks": "./hooks/hooks.json"
33+
"hooks": "./hooks/hooks.json",
34+
"mcpServers": "./mcp.json"
3335
}

README.md

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ turned out — so the next session starts smarter.
88
Powered by the [Genome Evolution Protocol (GEP)](https://evomap.ai) and the
99
[`@evomap/evolver`](https://github.com/EvoMap/evolver) engine.
1010

11-
> **Status:** v0.1.0 — hooks + skill + rule. Works standalone (local memory).
12-
> The MCP tool surface is provided separately by
13-
> [`@evomap/gep-mcp-server`](https://github.com/EvoMap/gep-mcp-server) and is
14-
> intentionally **not** bundled here (see *Architecture* below).
11+
> **Status:** v0.2.0 — hooks + skill + commands + MCP bridge. Works standalone
12+
> (local memory) and, when the Proxy is running, exposes the EvoMap mailbox
13+
> (genes/capsules) as MCP tools.
1514
1615
## What it does
1716

@@ -26,7 +25,10 @@ Three hooks run automatically — you don't invoke them:
2625
It also ships:
2726

2827
- A **`capability-evolver` skill** describing the recall → work → record loop.
29-
- An **`/evolve` command** for a deliberate evolution checkpoint.
28+
- An **MCP bridge** (`evolver-proxy`) exposing the local Proxy mailbox as tools:
29+
`evolver_search_assets`, `evolver_status`, `evolver_fetch_asset`,
30+
`evolver_publish_asset`, `evolver_poll`.
31+
- **`/evolve`** and **`/search`** commands.
3032
- A **rule** that reminds the agent to use evolution memory on substantive work.
3133

3234
## Install
@@ -88,28 +90,25 @@ The `stop` hook will then record outcomes to the Hub (with a local fallback if
8890
the Hub is unreachable). See the [evolver docs](https://evomap.ai) for node
8991
registration.
9092

91-
## Architecture (why no bundled MCP server)
93+
## Architecture (the MCP bridge vs. gep-mcp-server)
9294

93-
EvoMap deliberately splits two products:
94-
95-
- **`@evomap/evolver`** — the GPL-licensed, source-available evolution engine
96-
(daemon + CLI). This plugin does **not** bundle it; the plugin's own hooks are
97-
an independent MIT clean-room implementation that records memory in the same
98-
format the engine reads, so the two interoperate when you install it.
99-
- **`@evomap/gep-mcp-server`** — an Apache-licensed, standalone **protocol
100-
layer** that exposes GEP capabilities as MCP tools to any MCP client.
101-
102-
This plugin ships its own lightweight session-lifecycle hooks (the glue Cursor
103-
needs), which work standalone and degrade gracefully. If you also want the
104-
`gep_*` MCP tools inside Cursor, add `@evomap/gep-mcp-server` to your Cursor MCP
105-
config directly — it is not re-bundled here to avoid duplicating that
106-
separately-maintained product.
95+
- **This plugin's `evolver-proxy` bridge** is a thin, MIT, zero-dependency glue
96+
that exposes the *local* Proxy mailbox (the genes/capsules already synced to
97+
your machine) as MCP tools, reading the live url + auth token from
98+
`~/.evolver/settings.json`. It degrades gracefully when the Proxy is down.
99+
- **`@evomap/gep-mcp-server`** is the standalone, Apache-licensed **full GEP
100+
protocol layer** — the complete `gep_*` tool surface for any MCP client. Add it
101+
to your MCP config directly if you want that richer surface; the two compose.
102+
- **`@evomap/evolver`** is the GPL-licensed engine (daemon + CLI). The plugin's
103+
hooks are an independent MIT clean-room implementation that records memory in
104+
the same format the engine reads, so they interoperate when you install it.
107105

108106
## Environment variables
109107

110108
| Variable | Default | Purpose |
111109
|---|---|---|
112110
| `MEMORY_GRAPH_PATH` | (auto) | Override the memory graph file location. |
111+
| `EVOMAP_PROXY_PORT` | `19820` | Proxy port the MCP bridge falls back to (live url read from `~/.evolver/settings.json`). |
113112
| `EVOMAP_HUB_URL` / `EVOMAP_API_KEY` / `EVOMAP_NODE_ID` | (unset) | Enable Hub recording. |
114113
| `EVOLVER_HOOK_VERBOSE` | `0` | Set `1` to surface the session-end receipt inline (suppressed on Cursor by default). |
115114

commands/search.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
description: Search the EvoMap network for reusable evolution assets (genes/capsules) matching signals, via the evolver-proxy MCP tools.
3+
---
4+
5+
# /search — search EvoMap
6+
7+
Before doing substantive work from scratch, search the network for proven
8+
approaches.
9+
10+
Treat the arguments as space-separated signal keywords (e.g.
11+
`log_error perf_bottleneck`). If none are given, infer 2–4 from the current task.
12+
Valid signals: `log_error`, `perf_bottleneck`, `test_failure`, `capability_gap`,
13+
`user_feature_request`, `deployment_issue`, `recurring_error`.
14+
15+
1. Call the `evolver_search_assets` MCP tool (from the `evolver-proxy` server)
16+
with those signals.
17+
2. Summarize each hit: id, type (Gene/Capsule), a one-line description, relevance.
18+
3. If a hit applies, fetch its full content with `evolver_fetch_asset` and adapt
19+
it to the current task.
20+
21+
If the tool reports the Proxy is unreachable, tell the user to run `evolver` once
22+
in a git repo to start it — the local memory hooks keep working regardless.

mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"evolver-proxy": {
4+
"command": "node",
5+
"args": ["${CURSOR_PLUGIN_ROOT}/mcp/evolver-proxy.mjs"]
6+
}
7+
}
8+
}

mcp/evolver-proxy.mjs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/env node
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) 2026 EvoMap
4+
/**
5+
* Evolver Proxy MCP bridge (stdio, zero dependencies).
6+
*
7+
* Exposes the EvoMap local Proxy mailbox — genes, capsules, status — as MCP
8+
* tools so Claude can search/reuse/publish evolution assets natively.
9+
*
10+
* Transport: newline-delimited JSON-RPC 2.0 over stdin/stdout (MCP stdio).
11+
* All diagnostics go to stderr; stdout carries protocol traffic ONLY.
12+
*
13+
* The Proxy is a separate local process started by the @evomap/evolver CLI.
14+
* This bridge never spawns it; when it is down, tools return a helpful error.
15+
*/
16+
17+
import { readFileSync } from 'node:fs';
18+
import { homedir } from 'node:os';
19+
import { join } from 'node:path';
20+
import { createInterface } from 'node:readline';
21+
22+
const SERVER = { name: 'evolver-proxy', version: '0.1.0' };
23+
const DEFAULT_PROTOCOL = '2025-06-18';
24+
25+
function log(...a) { process.stderr.write('[evolver-proxy-mcp] ' + a.join(' ') + '\n'); }
26+
27+
/**
28+
* Resolve the live Proxy connection. ~/.evolver/settings.json is authoritative:
29+
* the running Proxy writes both its url and a per-instance auth token there.
30+
* Recent Proxy builds reject unauthenticated local requests with 401, so we
31+
* send `Authorization: Bearer <token>`. Re-read every call — the token rotates
32+
* whenever the Proxy restarts. Never log or echo the token.
33+
*/
34+
function readProxySettings() {
35+
let url = null, token = null;
36+
try {
37+
const s = JSON.parse(readFileSync(join(homedir(), '.evolver', 'settings.json'), 'utf8'));
38+
if (s?.proxy?.url) url = String(s.proxy.url).replace(/\/+$/, '');
39+
if (s?.proxy?.token) token = String(s.proxy.token);
40+
} catch { /* not running / unreadable — fall through */ }
41+
if (!url) url = `http://127.0.0.1:${process.env.EVOMAP_PROXY_PORT || '19820'}`;
42+
return { url, token };
43+
}
44+
45+
async function proxyFetch(method, path, body) {
46+
const { url: base, token } = readProxySettings();
47+
const ctrl = new AbortController();
48+
const timer = setTimeout(() => ctrl.abort(), 8000);
49+
try {
50+
const headers = {};
51+
if (body) headers['Content-Type'] = 'application/json';
52+
if (token) headers['Authorization'] = `Bearer ${token}`;
53+
const res = await fetch(base + path, {
54+
method,
55+
headers: Object.keys(headers).length ? headers : undefined,
56+
body: body ? JSON.stringify(body) : undefined,
57+
signal: ctrl.signal,
58+
});
59+
const text = await res.text();
60+
let data; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
61+
if (!res.ok) {
62+
// Make auth/connection failures actionable. Never echo the token.
63+
let hint = '';
64+
if ([401, 403].includes(res.status)) {
65+
hint = token
66+
? ' The Proxy token in ~/.evolver/settings.json looks stale (the Proxy mints a fresh token on restart). Restart this Claude session so the bridge re-reads it, or run /evolver:status.'
67+
: ` No Proxy token found in ~/.evolver/settings.json and the request was rejected — another process may be using ${base}. Start the Proxy (run \`evolver\` once in a git repo) or set EVOMAP_PROXY_PORT, then run /evolver:status.`;
68+
} else if (res.status === 404) {
69+
hint = ` Endpoint not found at ${base} — it may not be the Evolver Proxy. Confirm with /evolver:status.`;
70+
}
71+
return { ok: false, error: `Proxy at ${base} returned HTTP ${res.status}: ${typeof data === 'object' ? JSON.stringify(data) : text}.${hint}` };
72+
}
73+
return { ok: true, data };
74+
} catch (e) {
75+
const hint = `Evolver Proxy not reachable at ${base}. Start it by running \`evolver\` once inside a git repo (the CLI launches the Proxy), or run /evolver:status. Set EVOMAP_PROXY_PORT if you use a non-default port.`;
76+
return { ok: false, error: `${e.name === 'AbortError' ? 'Proxy request timed out' : 'Proxy connection failed: ' + e.message}. ${hint}` };
77+
} finally {
78+
clearTimeout(timer);
79+
}
80+
}
81+
82+
// ---- Tool registry -------------------------------------------------------
83+
84+
const TOOLS = [
85+
{
86+
name: 'evolver_status',
87+
description: 'Get the EvoMap Proxy status: running state, node_id, pending inbound/outbound message counts, and last Hub sync time. Use this first to confirm the Proxy is up.',
88+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
89+
handler: () => proxyFetch('GET', '/proxy/status'),
90+
},
91+
{
92+
name: 'evolver_search_assets',
93+
description: 'Search the EvoMap network for reusable evolution assets (Genes and Capsules) that match the given signals. Call this BEFORE starting substantive work to reuse proven approaches instead of reinventing them.',
94+
inputSchema: {
95+
type: 'object',
96+
properties: {
97+
signals: { type: 'array', items: { type: 'string' }, description: 'Signal keywords, e.g. ["log_error","perf_bottleneck","test_failure"].' },
98+
mode: { type: 'string', enum: ['semantic', 'exact'], default: 'semantic' },
99+
limit: { type: 'integer', minimum: 1, maximum: 25, default: 5 },
100+
},
101+
required: ['signals'],
102+
additionalProperties: false,
103+
},
104+
handler: (a) => proxyFetch('POST', '/asset/search', {
105+
signals: a.signals, mode: a.mode || 'semantic', limit: a.limit || 5,
106+
}),
107+
},
108+
{
109+
name: 'evolver_fetch_asset',
110+
description: 'Fetch the full content of one or more evolution assets by their IDs (e.g. "sha256:abc..."), as returned by evolver_search_assets.',
111+
inputSchema: {
112+
type: 'object',
113+
properties: { asset_ids: { type: 'array', items: { type: 'string' }, minItems: 1 } },
114+
required: ['asset_ids'],
115+
additionalProperties: false,
116+
},
117+
handler: (a) => proxyFetch('POST', '/asset/fetch', { asset_ids: a.asset_ids }),
118+
},
119+
{
120+
name: 'evolver_publish_asset',
121+
description: 'Publish one or more evolution assets (Genes/Capsules) to the EvoMap Hub for review. Queued locally and synced by the Proxy in the background; poll asset_submit_result with evolver_poll to see the Hub decision.',
122+
inputSchema: {
123+
type: 'object',
124+
properties: {
125+
assets: {
126+
type: 'array', minItems: 1,
127+
items: {
128+
type: 'object',
129+
properties: {
130+
type: { type: 'string', enum: ['Gene', 'Capsule'] },
131+
content: { type: 'string' },
132+
summary: { type: 'string' },
133+
signals: { type: 'array', items: { type: 'string' } },
134+
},
135+
required: ['type', 'content'],
136+
},
137+
},
138+
},
139+
required: ['assets'],
140+
additionalProperties: false,
141+
},
142+
handler: (a) => proxyFetch('POST', '/asset/submit', { assets: a.assets }),
143+
},
144+
{
145+
name: 'evolver_poll',
146+
description: 'Poll the local mailbox for inbound messages by type, e.g. "asset_submit_result" (Hub review decisions), "hub_event", or "task_available". Returns and does not auto-acknowledge.',
147+
inputSchema: {
148+
type: 'object',
149+
properties: {
150+
type: { type: 'string', description: 'Message type filter, e.g. "asset_submit_result".' },
151+
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
152+
},
153+
additionalProperties: false,
154+
},
155+
handler: (a) => proxyFetch('POST', '/mailbox/poll', { type: a.type, limit: a.limit || 10 }),
156+
},
157+
];
158+
159+
const TOOL_BY_NAME = Object.fromEntries(TOOLS.map(t => [t.name, t]));
160+
161+
// ---- JSON-RPC plumbing ---------------------------------------------------
162+
163+
function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); }
164+
function reply(id, result) { send({ jsonrpc: '2.0', id, result }); }
165+
function replyError(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }); }
166+
167+
async function handleToolCall(id, params) {
168+
const tool = TOOL_BY_NAME[params?.name];
169+
if (!tool) return replyError(id, -32602, `Unknown tool: ${params?.name}`);
170+
let out;
171+
try {
172+
out = await tool.handler(params.arguments || {});
173+
} catch (e) {
174+
out = { ok: false, error: `Tool execution failed: ${e.message}` };
175+
}
176+
const text = out.ok ? JSON.stringify(out.data, null, 2) : out.error;
177+
reply(id, { content: [{ type: 'text', text }], isError: !out.ok });
178+
}
179+
180+
async function dispatch(req) {
181+
const { id, method, params } = req;
182+
const isNotification = id === undefined || id === null;
183+
184+
switch (method) {
185+
case 'initialize':
186+
return reply(id, {
187+
protocolVersion: params?.protocolVersion || DEFAULT_PROTOCOL,
188+
capabilities: { tools: {} },
189+
serverInfo: SERVER,
190+
instructions: 'Evolver Proxy bridge. Use evolver_search_assets before substantive work to reuse proven genes/capsules; evolver_status to check the Proxy; evolver_publish_asset to contribute new ones.',
191+
});
192+
case 'notifications/initialized':
193+
case 'initialized':
194+
return; // notification — no response
195+
case 'ping':
196+
return reply(id, {});
197+
case 'tools/list':
198+
return reply(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
199+
case 'tools/call':
200+
return handleToolCall(id, params);
201+
default:
202+
if (isNotification) return; // ignore unknown notifications
203+
return replyError(id, -32601, `Method not found: ${method}`);
204+
}
205+
}
206+
207+
// Track in-flight (async) requests so we never exit on stdin close while a
208+
// tool call's reply is still pending — otherwise the last response is dropped.
209+
let pending = 0;
210+
let closed = false;
211+
function maybeExit() { if (closed && pending === 0) process.exit(0); }
212+
213+
const rl = createInterface({ input: process.stdin });
214+
rl.on('line', (line) => {
215+
const trimmed = line.trim();
216+
if (!trimmed) return;
217+
let req;
218+
try { req = JSON.parse(trimmed); } catch { log('dropping non-JSON line'); return; }
219+
pending++;
220+
Promise.resolve(dispatch(req))
221+
.catch(e => {
222+
log('dispatch error:', e.message);
223+
if (req && req.id != null) replyError(req.id, -32603, `Internal error: ${e.message}`);
224+
})
225+
.finally(() => { pending--; maybeExit(); });
226+
});
227+
rl.on('close', () => { closed = true; maybeExit(); });
228+
229+
log(`ready (server ${SERVER.version}); proxy base ${readProxySettings().url}`);

0 commit comments

Comments
 (0)