@evomap/evolver-mcp 2.0.0-beta.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.
- package/dist/codexInstaller.d.ts +34 -0
- package/dist/codexInstaller.js +171 -0
- package/dist/cursorRulesInstaller.d.ts +76 -0
- package/dist/cursorRulesInstaller.js +196 -0
- package/dist/envFile.d.ts +10 -0
- package/dist/envFile.js +68 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/injection.d.ts +56 -0
- package/dist/injection.js +84 -0
- package/dist/installer.d.ts +106 -0
- package/dist/installer.js +513 -0
- package/dist/manualWiring.d.ts +14 -0
- package/dist/manualWiring.js +91 -0
- package/dist/primer.d.ts +12 -0
- package/dist/primer.js +32 -0
- package/dist/proxyClient.d.ts +72 -0
- package/dist/proxyClient.js +193 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +38 -0
- package/dist/serviceGuidance.d.ts +15 -0
- package/dist/serviceGuidance.js +170 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +107 -0
- package/dist/tools.d.ts +39 -0
- package/dist/tools.js +401 -0
- package/package.json +35 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { assetstore, wire, mailbox as mb, hub, bootstrap, ops } from '@evomap/evolver-core';
|
|
2
|
+
import { buildEvolverPrimer } from './primer.js';
|
|
3
|
+
const str = (v) => (typeof v === 'string' ? v : String(v ?? ''));
|
|
4
|
+
const strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
|
|
5
|
+
const REUSE_OUTCOMES = new Set(['success', 'failed', 'mismatched', 'stale', 'unsafe']);
|
|
6
|
+
function record(value) {
|
|
7
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
function resultArray(value) {
|
|
10
|
+
const r = record(value);
|
|
11
|
+
if (Array.isArray(r['results']))
|
|
12
|
+
return r['results'];
|
|
13
|
+
if (Array.isArray(r['assets']))
|
|
14
|
+
return r['assets'];
|
|
15
|
+
const payload = record(r['payload']);
|
|
16
|
+
if (Array.isArray(payload['results']))
|
|
17
|
+
return payload['results'];
|
|
18
|
+
if (Array.isArray(payload['assets']))
|
|
19
|
+
return payload['assets'];
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
function firstAsset(value) {
|
|
23
|
+
const r = record(value);
|
|
24
|
+
if (Array.isArray(r['assets']))
|
|
25
|
+
return r['assets'][0] ?? null;
|
|
26
|
+
if (Array.isArray(r['results']))
|
|
27
|
+
return r['results'][0] ?? null;
|
|
28
|
+
const payload = record(r['payload']);
|
|
29
|
+
if (Array.isArray(payload['assets']))
|
|
30
|
+
return payload['assets'][0] ?? null;
|
|
31
|
+
if (Array.isArray(payload['results']))
|
|
32
|
+
return payload['results'][0] ?? null;
|
|
33
|
+
if (r['asset'])
|
|
34
|
+
return r['asset'];
|
|
35
|
+
if (payload['asset'])
|
|
36
|
+
return payload['asset'];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function requestedAsset(value, assetId) {
|
|
40
|
+
const asset = firstAsset(value);
|
|
41
|
+
const recordAsset = record(asset);
|
|
42
|
+
return recordAsset['asset_id'] === assetId ? asset : null;
|
|
43
|
+
}
|
|
44
|
+
function optionalNonNegativeNumberArg(args, key, error) {
|
|
45
|
+
if (!Object.prototype.hasOwnProperty.call(args, key))
|
|
46
|
+
return undefined;
|
|
47
|
+
const value = args[key];
|
|
48
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
49
|
+
throw new Error(error);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function reuseOutcome(value) {
|
|
53
|
+
if (typeof value === 'string' && REUSE_OUTCOMES.has(value)) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
throw new Error('invalid reuse outcome');
|
|
57
|
+
}
|
|
58
|
+
function validateAssetBundleArgs(args) {
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(args, 'assets')) {
|
|
60
|
+
const assets = args['assets'];
|
|
61
|
+
if (!Array.isArray(assets) || assets.length === 0 || assets.some((asset) => asset == null)) {
|
|
62
|
+
throw new Error('evolver_asset_validate requires asset or non-empty assets');
|
|
63
|
+
}
|
|
64
|
+
return assets;
|
|
65
|
+
}
|
|
66
|
+
if (Object.prototype.hasOwnProperty.call(args, 'asset')) {
|
|
67
|
+
const asset = args['asset'];
|
|
68
|
+
if (asset == null)
|
|
69
|
+
throw new Error('evolver_asset_validate requires asset or non-empty assets');
|
|
70
|
+
return [asset];
|
|
71
|
+
}
|
|
72
|
+
throw new Error('evolver_asset_validate requires asset or non-empty assets');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Evolver MCP 工具集(M5-2). asset.search/fetch/publish + gep.build + mailbox.*.
|
|
76
|
+
* schema 单一来源走 gep-sdk(经 evolver-core 重导出), 不重复实现.
|
|
77
|
+
*/
|
|
78
|
+
export function buildEvolverTools(deps) {
|
|
79
|
+
const now = deps.now ?? (() => Date.now());
|
|
80
|
+
const searchableKinds = ['Gene', 'Capsule', 'EvolutionEvent', 'AntiGene'];
|
|
81
|
+
// Per-connection idempotency for reuse-feedback emissions (#268): a retried reuse_result must not double-record.
|
|
82
|
+
// Keyed by (eventType, connId, assetId, taskId); deduped ONLY when the agent supplies a taskId (the explicit
|
|
83
|
+
// dedup handle) — without it we cannot tell a retry from a genuine new reuse, so we must not drop it (Bugbot
|
|
84
|
+
// #269). Bounded by one stdio process' lifetime.
|
|
85
|
+
const emitted = new Set();
|
|
86
|
+
// Best-effort + never throws: reuse-feedback is an optimization, so an ingest failure can never break the tool
|
|
87
|
+
// the agent is calling (mirrors the autoexec reuse seam contract). Returns whether the event was recorded
|
|
88
|
+
// (emitted now OR already recorded via idempotency; false = no ingestor, or the ingest threw).
|
|
89
|
+
const emitReuse = async (type, assetId, taskId, extra, title) => {
|
|
90
|
+
if (!deps.ingestor)
|
|
91
|
+
return false;
|
|
92
|
+
const cycleId = `${deps.cycleId ?? 'mcp'}${taskId ? `:${taskId}` : ''}`;
|
|
93
|
+
const key = taskId ? `${type}|${deps.cycleId ?? 'mcp'}|${assetId}|${taskId}` : null;
|
|
94
|
+
if (key && emitted.has(key))
|
|
95
|
+
return true; // already recorded this (type,assetId,taskId) on a prior call
|
|
96
|
+
if (key)
|
|
97
|
+
emitted.add(key);
|
|
98
|
+
try {
|
|
99
|
+
// The root_event schema caps human.title at 80 chars (humanNarrative). A content-addressed assetId is a
|
|
100
|
+
// 71-char `sha256:…`, so a naive `<prefix>: <assetId>` title overflows and the ingest THROWS — which this
|
|
101
|
+
// best-effort path would SILENTLY swallow, leaving the local ledger un-credited for every MCP reuse of a
|
|
102
|
+
// real asset (#268 regression). Clamp defensively so a long title can never drop the event; the full
|
|
103
|
+
// assetId is always in the payload regardless.
|
|
104
|
+
await deps.ingestor.ingest({ type, human: { title: title.slice(0, 80), detail: `cycle ${cycleId}` }, payload: { assetId, cycleId, ...extra } });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
if (key)
|
|
109
|
+
emitted.delete(key);
|
|
110
|
+
return false; /* keep retryable; report not recorded */
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// SUCCESS → value.reuse_hit (observed reuse; no measured baseline means zero/unknown savings, never a
|
|
114
|
+
// fabricated ROI number). NON-success → value.reuse_outcome (the keep/prune verdict half of the cross-runtime
|
|
115
|
+
// signal, #268 slice C; summarizeReuseOutcomes reads it). Both best-effort + idempotent.
|
|
116
|
+
// Titles use a SHORT asset id (the full sha256:… lives in the payload) so they stay well under the 80-char
|
|
117
|
+
// title cap; emitReuse also clamps defensively.
|
|
118
|
+
const emitReuseHit = (assetId, taskId) => emitReuse(ops.VALUE_REUSE_HIT_EVENT, assetId, taskId, { fetchTokens: 0 }, `mcp reuse hit: ${assetId.slice(0, 19)}…`);
|
|
119
|
+
const emitReuseOutcome = (assetId, taskId, outcome) => emitReuse(ops.VALUE_REUSE_OUTCOME_EVENT, assetId, taskId, { outcome }, `mcp reuse ${outcome}: ${assetId.slice(0, 19)}…`);
|
|
120
|
+
// evolver_recall priming → a `value.inject` root_event recording which approved genes were primed (#mcp-recall),
|
|
121
|
+
// the SAME attribution rail the SessionStart hook feeds (ops.VALUE_INJECT_EVENT). This is what lets #274
|
|
122
|
+
// auto-recall later observe — from the MCP agent's own transcript (the generic-chat adapter) — which injected
|
|
123
|
+
// genes were actually used, closing the self-learning loop for any MCP agent, not just the hook-based runtimes.
|
|
124
|
+
// Best-effort + never throws (priming is the agent's path); deduped per connection on the (session, gene set) so
|
|
125
|
+
// repeated recall calls do not inflate the inject rail.
|
|
126
|
+
//
|
|
127
|
+
// sessionId is what ties the inject to a transcript: #274 auto-recall only emits value.recall when the inject
|
|
128
|
+
// payload's sessionId EQUALS the transcript basename (minus .jsonl) — sessionIdFromTranscript. The MCP server
|
|
129
|
+
// cannot know the agent's transcript filename, so the agent must pass its session key for the loop to close;
|
|
130
|
+
// without it the value.inject is attribution-only (recorded, but auto-recall cannot correlate it to a session).
|
|
131
|
+
const injectedSig = new Set();
|
|
132
|
+
const emitInject = async (geneIds, sessionId) => {
|
|
133
|
+
if (!deps.ingestor || geneIds.length === 0)
|
|
134
|
+
return false;
|
|
135
|
+
const cycleId = deps.cycleId ?? 'mcp';
|
|
136
|
+
const sig = `${sessionId ?? ''}|${[...geneIds].join(',')}`;
|
|
137
|
+
if (injectedSig.has(sig))
|
|
138
|
+
return true; // already recorded this (session, gene set) on this connection
|
|
139
|
+
injectedSig.add(sig);
|
|
140
|
+
try {
|
|
141
|
+
await deps.ingestor.ingest({
|
|
142
|
+
type: ops.VALUE_INJECT_EVENT,
|
|
143
|
+
human: { title: `mcp injected ${geneIds.length} gene(s)`, detail: `cycle ${cycleId}` },
|
|
144
|
+
payload: { geneIds: [...geneIds], cycleId, ...(sessionId ? { sessionId } : {}) },
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
injectedSig.delete(sig);
|
|
150
|
+
return false; /* keep retryable */
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const tools = [
|
|
154
|
+
{
|
|
155
|
+
// Self-onboarding (#mcp-onboarding): any MCP agent can learn the quiet reuse loop when it needs guidance.
|
|
156
|
+
// Mirrors the initialize.instructions primer; always present, but no longer asks the agent to narrate routine work.
|
|
157
|
+
name: 'evolver_guide',
|
|
158
|
+
description: '按需说明 evolver 的静默复用机制(search→reuse→capture 循环)与各工具何时调用;不要向用户叙述例行预检、状态或空搜索。',
|
|
159
|
+
inputSchema: { type: 'object', properties: {} },
|
|
160
|
+
handler: async () => ({ guide: buildEvolverPrimer({ proxy: !!deps.proxy }) }),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
// Priming for ANY MCP agent (#mcp-recall): when prior local memory is likely to help, return trusted +
|
|
164
|
+
// review-approved genes (the same gate the SessionStart hook uses) AND record a value.inject so auto-recall can
|
|
165
|
+
// later observe, from this agent's transcript, which primed genes were used. NOT a hub search (that is
|
|
166
|
+
// evolver_asset_search): this is the curated, already-approved local memory, ready to reuse quietly.
|
|
167
|
+
name: 'evolver_recall',
|
|
168
|
+
description: '当本机已审核通过的可复用经验 gene 可能有帮助时调用;返回信任+审核双闸后的本地 gene,并记录一条 value.inject。若传入 sessionId(= 本会话 transcript 文件名去掉 .jsonl 后缀),后续 auto-recall 能从该 transcript 观测哪些注入 gene 真正被用,为经 MCP 接入的 AI 闭合自学习环;不传则仅记录归因、无法关联到会话。命中后静默复用,并在结果明确后用 evolver_asset_reuse_result 回报。',
|
|
169
|
+
inputSchema: { type: 'object', properties: { limit: { type: 'number' }, sessionId: { type: 'string' } } },
|
|
170
|
+
handler: async (a) => {
|
|
171
|
+
const limit = optionalNonNegativeNumberArg(a, 'limit', 'evolver_recall limit must be a non-negative number') ?? 5;
|
|
172
|
+
const sessionId = typeof a['sessionId'] === 'string' && a['sessionId'].trim() ? a['sessionId'].trim() : undefined;
|
|
173
|
+
const review = assetstore.reviewLedgerForStore(deps.store);
|
|
174
|
+
const genes = await assetstore.listApprovedGenes(deps.store, review, limit);
|
|
175
|
+
const primed = genes.map((g) => {
|
|
176
|
+
const r = g;
|
|
177
|
+
const id = typeof r['id'] === 'string' ? r['id'] : String(r['asset_id']);
|
|
178
|
+
return {
|
|
179
|
+
id,
|
|
180
|
+
asset_id: String(r['asset_id']),
|
|
181
|
+
...(typeof r['summary'] === 'string' ? { summary: r['summary'] } : {}),
|
|
182
|
+
...(Array.isArray(r['signals_match']) ? { signals_match: r['signals_match'] } : {}),
|
|
183
|
+
...(Array.isArray(r['strategy']) ? { strategy: r['strategy'] } : {}),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const injected = await emitInject(primed.map((p) => p.id), sessionId);
|
|
187
|
+
return {
|
|
188
|
+
genes: primed,
|
|
189
|
+
count: primed.length,
|
|
190
|
+
injected,
|
|
191
|
+
correlated: injected && sessionId !== undefined, // auto-recall can tie this inject to the session only with a sessionId
|
|
192
|
+
note: primed.length === 0
|
|
193
|
+
? 'no approved genes yet — distill/approve some first (evolver_distill_conversation), then they appear here'
|
|
194
|
+
: sessionId === undefined
|
|
195
|
+
? 'reuse a matching gene, then report via evolver_asset_reuse_result. Pass sessionId (your transcript filename without .jsonl) so evolver can observe which primed genes you used.'
|
|
196
|
+
: 'reuse a matching gene, then report the outcome via evolver_asset_reuse_result',
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
...(deps.proxy ? [{
|
|
201
|
+
name: 'evolver_proxy_status',
|
|
202
|
+
description: '检查本机 evolver-proxy 与 PHub 的连接状态. 需要 EVOLVER_PROXY_URL/EVOLVER_IPC_TOKEN.',
|
|
203
|
+
inputSchema: { type: 'object', properties: {} },
|
|
204
|
+
handler: async () => deps.proxy.status(),
|
|
205
|
+
}] : []),
|
|
206
|
+
{
|
|
207
|
+
name: 'evolver_asset_search',
|
|
208
|
+
description: deps.proxy
|
|
209
|
+
? '通过本机 evolver-proxy 搜索 PHub 经验资产(Gene/Capsule/EvolutionEvent); AntiGene 是本地负经验资产, 会直接查本地库供人工 review.'
|
|
210
|
+
: '搜索本地经验资产库(Gene/Capsule/EvolutionEvent/AntiGene). 支持 kind/信号/类目/gene 反查/文本.',
|
|
211
|
+
inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: searchableKinds }, signalsAny: { type: 'array', items: { type: 'string' } }, category: { type: 'string' }, gene: { type: 'string' }, text: { type: 'string' }, limit: { type: 'number' } } },
|
|
212
|
+
handler: async (a) => {
|
|
213
|
+
if (deps.proxy && a['kind'] === 'AntiGene') {
|
|
214
|
+
return deps.store.search({
|
|
215
|
+
kind: 'AntiGene',
|
|
216
|
+
signalsAny: a['signalsAny'],
|
|
217
|
+
category: a['category'],
|
|
218
|
+
gene: a['gene'],
|
|
219
|
+
text: a['text'],
|
|
220
|
+
limit: a['limit'],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (deps.proxy) {
|
|
224
|
+
return resultArray(await deps.proxy.search({
|
|
225
|
+
signalsAny: strArray(a['signalsAny']),
|
|
226
|
+
...(typeof a['kind'] === 'string' ? { kind: a['kind'] } : {}),
|
|
227
|
+
...(typeof a['category'] === 'string' ? { category: a['category'] } : {}),
|
|
228
|
+
...(typeof a['gene'] === 'string' ? { gene: a['gene'] } : {}),
|
|
229
|
+
...(typeof a['text'] === 'string' ? { text: a['text'] } : {}),
|
|
230
|
+
...(typeof a['limit'] === 'number' ? { limit: a['limit'] } : {}),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
return deps.store.search({
|
|
234
|
+
kind: a['kind'],
|
|
235
|
+
signalsAny: a['signalsAny'],
|
|
236
|
+
category: a['category'],
|
|
237
|
+
gene: a['gene'],
|
|
238
|
+
text: a['text'],
|
|
239
|
+
limit: a['limit'],
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'evolver_asset_fetch',
|
|
245
|
+
description: deps.proxy ? '通过本机 evolver-proxy 按 asset_id 从 PHub 拉 full asset, 供当前 Agent 直接复用.' : '按 asset_id 取单个资产.',
|
|
246
|
+
inputSchema: { type: 'object', required: ['assetId'], properties: { assetId: { type: 'string' } } },
|
|
247
|
+
handler: async (a) => {
|
|
248
|
+
const assetId = str(a['assetId']);
|
|
249
|
+
if (deps.proxy) {
|
|
250
|
+
const local = await deps.store.get(assetId);
|
|
251
|
+
if (local?.type === 'AntiGene')
|
|
252
|
+
return local;
|
|
253
|
+
return requestedAsset(await deps.proxy.fetchAsset({ assetId }), assetId);
|
|
254
|
+
}
|
|
255
|
+
return deps.store.get(assetId);
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'evolver_gep_build',
|
|
260
|
+
description: '由字段构造资产并计算 asset_id(content-addressed). 不落库; 返回带 asset_id 的资产 + 校验结果. 用于发布前确认.',
|
|
261
|
+
inputSchema: { type: 'object', required: ['asset'], properties: { asset: { type: 'object' } } },
|
|
262
|
+
handler: async (a) => {
|
|
263
|
+
const asset = a['asset'];
|
|
264
|
+
const assetId = wire.computeAssetId(asset);
|
|
265
|
+
const validation = wire.validateWire(asset);
|
|
266
|
+
return { asset: { ...asset, asset_id: assetId }, asset_id: assetId, wire_valid: validation.ok, wire_errors: validation.errors };
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'evolver_asset_publish',
|
|
271
|
+
description: deps.proxy ? '把资产提交给本机 evolver-proxy, 由 proxy 异步发布到 PHub. Capsule.gene 须非空或 ad-hoc.' : '把资产发布到本地库(content-addressed 去重 + 强绑定校验). Capsule.gene 须非空或 ad-hoc.',
|
|
272
|
+
inputSchema: { type: 'object', required: ['asset'], properties: { asset: { type: 'object' } } },
|
|
273
|
+
handler: async (a) => deps.proxy ? deps.proxy.submitAsset(a['asset']) : deps.store.put(a['asset']),
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'evolver_distill_conversation',
|
|
277
|
+
description: '从当前 agent 对话中蒸馏可复用 Gene/Capsule. 需要具体 summary、strategy/evidence、artifacts、validation; core quality gate 会拒绝弱信号.',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
required: ['summary'],
|
|
281
|
+
properties: {
|
|
282
|
+
title: { type: 'string' },
|
|
283
|
+
summary: { type: 'string' },
|
|
284
|
+
platform: { type: 'string' },
|
|
285
|
+
thread_id: { type: 'string' },
|
|
286
|
+
user_prompt: { type: 'string' },
|
|
287
|
+
assistant_summary: { type: 'string' },
|
|
288
|
+
transcript: { type: 'string' },
|
|
289
|
+
signals: { type: 'array', items: { type: 'string' } },
|
|
290
|
+
strategy: { type: 'array', items: { type: 'string' } },
|
|
291
|
+
artifacts: { type: 'array', items: { type: 'string' } },
|
|
292
|
+
validation: { type: 'array', items: { type: 'string' } },
|
|
293
|
+
persist: { type: 'boolean' },
|
|
294
|
+
publish: { type: 'boolean' },
|
|
295
|
+
min_score: { type: 'integer', minimum: 1, maximum: 10 },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
handler: async (a) => {
|
|
299
|
+
const input = { ...a, platform: a['platform'] || 'mcp', model: bootstrap.detectModelName() };
|
|
300
|
+
if (deps.proxy)
|
|
301
|
+
return deps.proxy.distillConversation(input);
|
|
302
|
+
return hub.distillConversation(input, { persist: a['persist'] === true, store: deps.store });
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
// Reuse-result is the MCP-native recall signal (#268): the agent self-reports whether a reused asset worked.
|
|
307
|
+
// Registered in ALL modes (was proxy-only) so MCP-only agents — no SessionStart hook, no daemon — can close the
|
|
308
|
+
// loop. A SUCCESS credits the LOCAL ledger via emitLocalReuse; proxy mode ALSO forwards to PHub for cross-node
|
|
309
|
+
// aggregation (prior behavior + return shape preserved).
|
|
310
|
+
tools.push({
|
|
311
|
+
name: 'evolver_asset_reuse_result',
|
|
312
|
+
description: '上报某复用资产的实际结果(success/failed/mismatched/stale/unsafe). 任何模式下 success 会在本地 value-ledger 记一笔 reuse(让经 MCP 接入的任何 AI 都能反哺本地经验环, #268);proxy 模式还会转发到 PHub.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
required: ['assetId', 'outcome'],
|
|
316
|
+
properties: {
|
|
317
|
+
assetId: { type: 'string' },
|
|
318
|
+
outcome: { type: 'string', enum: ['success', 'failed', 'mismatched', 'stale', 'unsafe'] },
|
|
319
|
+
taskId: { type: 'string' },
|
|
320
|
+
traceId: { type: 'string' },
|
|
321
|
+
tokensSaved: { type: 'number', minimum: 0, description: 'Deprecated compatibility field. Ignored unless future measurement metadata proves a measured baseline.' },
|
|
322
|
+
timeSavedSeconds: { type: 'number', minimum: 0 },
|
|
323
|
+
reason: { type: 'string' },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
handler: async (a) => {
|
|
327
|
+
// Validate numeric args FIRST so a bad request rejects before any local emit or hub forward.
|
|
328
|
+
// Keep validating the deprecated field for caller compatibility, but never forward it as ROI by itself.
|
|
329
|
+
optionalNonNegativeNumberArg(a, 'tokensSaved', 'invalid_tokens_saved');
|
|
330
|
+
const timeSavedSeconds = optionalNonNegativeNumberArg(a, 'timeSavedSeconds', 'invalid_time_saved_seconds');
|
|
331
|
+
const assetId = str(a['assetId']);
|
|
332
|
+
const outcome = reuseOutcome(a['outcome']);
|
|
333
|
+
const taskId = typeof a['taskId'] === 'string' ? a['taskId'] : undefined;
|
|
334
|
+
// Local-first: feed the local experience loop even with no proxy/hub (the MCP-only path). SUCCESS credits the
|
|
335
|
+
// $ rail (reuse_hit); a non-success records the keep/prune verdict (reuse_outcome) — the cross-runtime signal.
|
|
336
|
+
let creditedLocally = false;
|
|
337
|
+
if (outcome === 'success')
|
|
338
|
+
creditedLocally = await emitReuseHit(assetId, taskId);
|
|
339
|
+
else
|
|
340
|
+
await emitReuseOutcome(assetId, taskId, outcome);
|
|
341
|
+
if (deps.proxy) {
|
|
342
|
+
return deps.proxy.recordReuseResult({
|
|
343
|
+
assetId, outcome,
|
|
344
|
+
...(taskId !== undefined ? { taskId } : {}),
|
|
345
|
+
...(typeof a['traceId'] === 'string' ? { traceId: a['traceId'] } : {}),
|
|
346
|
+
...(timeSavedSeconds !== undefined ? { timeSavedSeconds } : {}),
|
|
347
|
+
...(typeof a['reason'] === 'string' ? { reason: a['reason'] } : {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return { recorded: true, local: creditedLocally, outcome };
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
if (deps.mailbox) {
|
|
354
|
+
const box = deps.mailbox;
|
|
355
|
+
tools.push({
|
|
356
|
+
name: 'evolver_mailbox_send',
|
|
357
|
+
description: '投递一条 mailbox 消息(类型须在目录内). 副作用类型应传 idempotencyKey.',
|
|
358
|
+
inputSchema: { type: 'object', required: ['type'], properties: { type: { type: 'string' }, payload: { type: 'object' }, idempotencyKey: { type: 'string' }, runtimeNamespace: { type: 'string' } } },
|
|
359
|
+
handler: async (a) => {
|
|
360
|
+
const env = mb.createEnvelope({
|
|
361
|
+
type: str(a['type']), payload: a['payload'],
|
|
362
|
+
...(a['idempotencyKey'] ? { idempotencyKey: str(a['idempotencyKey']) } : {}),
|
|
363
|
+
...(a['runtimeNamespace'] ? { runtimeNamespace: str(a['runtimeNamespace']) } : {}),
|
|
364
|
+
now: now(),
|
|
365
|
+
});
|
|
366
|
+
const r = box.send(env);
|
|
367
|
+
return { id: env.id, receiptId: r.receiptId, stored: r.stored, correlationId: env.correlationId };
|
|
368
|
+
},
|
|
369
|
+
}, {
|
|
370
|
+
name: 'evolver_mailbox_status',
|
|
371
|
+
description: '查 mailbox 消息状态(status/attempts/dlq).',
|
|
372
|
+
inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },
|
|
373
|
+
handler: async (a) => box.getStatus(str(a['id'])) ?? { error: 'not found' },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (deps.proxy) {
|
|
377
|
+
tools.push({
|
|
378
|
+
name: 'evolver_asset_validate',
|
|
379
|
+
description: '通过本机 evolver-proxy 对 PHub 做发布前 dry-run 校验: 先执行与发布相同的本地脱敏/泄漏拦截, 再跑 hub 端质量门禁 + 内容安全扫描, 不落库、不计费. 返回 {valid, reason?}. 建议在 evolver_asset_publish 前调用. Capsule.gene 须非空或 ad-hoc.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
anyOf: [{ required: ['assets'] }, { required: ['asset'] }],
|
|
383
|
+
properties: {
|
|
384
|
+
assets: { type: 'array', minItems: 1, items: { type: 'object' } },
|
|
385
|
+
asset: { type: 'object' },
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
handler: async (a) => deps.proxy.validateAssetBundle({ assets: validateAssetBundleArgs(a) }),
|
|
389
|
+
}, {
|
|
390
|
+
name: 'evolver_mailbox_poll',
|
|
391
|
+
description: '通过本机 evolver-proxy 轮询 PHub mailbox 的待处理消息.',
|
|
392
|
+
inputSchema: { type: 'object', properties: { type: { type: 'string' }, direction: { type: 'string' }, limit: { type: 'number' } } },
|
|
393
|
+
handler: async (a) => deps.proxy.call('POST', '/mailbox/poll', {
|
|
394
|
+
...(typeof a['type'] === 'string' ? { type: a['type'] } : {}),
|
|
395
|
+
...(typeof a['direction'] === 'string' ? { direction: a['direction'] } : {}),
|
|
396
|
+
...(typeof a['limit'] === 'number' ? { limit: a['limit'] } : {}),
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return tools;
|
|
401
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evomap/evolver-mcp",
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Evolver MCP server (agent 工具发现入口)",
|
|
7
|
+
"bin": {
|
|
8
|
+
"evolver-mcp": "./dist/stdio.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./stdio": {
|
|
18
|
+
"types": "./dist/stdio.d.ts",
|
|
19
|
+
"default": "./dist/stdio.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@evomap/evolver-core": "2.0.0-beta.0",
|
|
24
|
+
"smol-toml": "^1.6.1"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"tag": "v2-beta"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"package.json"
|
|
34
|
+
]
|
|
35
|
+
}
|