@remnic/core 9.3.768 → 9.3.769
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/access-cli.js +5 -5
- package/dist/access-http.d.ts +51 -1
- package/dist/access-http.js +2 -2
- package/dist/access-mcp.d.ts +7 -1
- package/dist/access-mcp.js +5 -3
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-schema.d.ts +68 -68
- package/dist/{chunk-TVLN5EZZ.js → chunk-CNXMWYLA.js} +78 -3
- package/dist/chunk-CNXMWYLA.js.map +1 -0
- package/dist/{chunk-UG274TNV.js → chunk-E2SPGGUI.js} +13 -8
- package/dist/chunk-E2SPGGUI.js.map +1 -0
- package/dist/{chunk-HBOPSFQQ.js → chunk-EVXI2I6G.js} +3 -3
- package/dist/{chunk-4DLFJJOQ.js → chunk-QMN3CIFS.js} +2 -2
- package/dist/{chunk-J3UJJZKI.js → chunk-SIPZ5UMK.js} +85 -5
- package/dist/chunk-SIPZ5UMK.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/connectors/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -5
- package/dist/orchestrator.js +5 -5
- package/dist/schemas.d.ts +84 -84
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/tokens.d.ts +3 -1
- package/dist/tokens.js +3 -1
- package/dist/transfer/types.d.ts +66 -66
- package/package.json +2 -2
- package/src/access-http.test.ts +350 -0
- package/src/access-http.ts +160 -6
- package/src/access-mcp.ts +120 -3
- package/src/index.ts +3 -0
- package/src/tokens.test.ts +58 -0
- package/src/tokens.ts +19 -8
- package/dist/chunk-J3UJJZKI.js.map +0 -1
- package/dist/chunk-TVLN5EZZ.js.map +0 -1
- package/dist/chunk-UG274TNV.js.map +0 -1
- /package/dist/{chunk-HBOPSFQQ.js.map → chunk-EVXI2I6G.js.map} +0 -0
- /package/dist/{chunk-4DLFJJOQ.js.map → chunk-QMN3CIFS.js.map} +0 -0
package/src/access-mcp.ts
CHANGED
|
@@ -78,7 +78,79 @@ type McpResource = {
|
|
|
78
78
|
_meta?: Record<string, unknown>;
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Conservative allowlist of canonical MCP tool suffixes that are
|
|
83
|
+
* unambiguously read-only. Tools in this set are tagged with
|
|
84
|
+
* `annotations: { readOnlyHint: true }` so ChatGPT (and other MCP
|
|
85
|
+
* clients that honor the hint) can skip per-call confirmation.
|
|
86
|
+
*
|
|
87
|
+
* The list is suffix-based so it covers both the `remnic.*` and
|
|
88
|
+
* `engram.*` naming forms. Anything not on it stays unannotated:
|
|
89
|
+
* uncertainty is resolved as "might mutate".
|
|
90
|
+
*
|
|
91
|
+
* Excluded by construction: anything that writes, runs a pipeline,
|
|
92
|
+
* flushes, applies, records, imports, or destructively deletes.
|
|
93
|
+
*/
|
|
94
|
+
const MCP_READ_ONLY_TOOL_SUFFIXES: Readonly<Record<string, true>> = {
|
|
95
|
+
recall: true,
|
|
96
|
+
recall_explain: true,
|
|
97
|
+
recall_tier_explain: true,
|
|
98
|
+
recall_xray: true,
|
|
99
|
+
briefing: true,
|
|
100
|
+
wearables_status: true,
|
|
101
|
+
transcript_day: true,
|
|
102
|
+
transcript_search: true,
|
|
103
|
+
transcript_memories: true,
|
|
104
|
+
action_confidence: true,
|
|
105
|
+
capsule_list: true,
|
|
106
|
+
procedural_stats: true,
|
|
107
|
+
memory_get: true,
|
|
108
|
+
memory_timeline: true,
|
|
109
|
+
entity_get: true,
|
|
110
|
+
review_queue_list: true,
|
|
111
|
+
lcm_search: true,
|
|
112
|
+
continuity_incident_list: true,
|
|
113
|
+
identity_anchor_get: true,
|
|
114
|
+
memory_identity: true,
|
|
115
|
+
memory_search: true,
|
|
116
|
+
memory_profile: true,
|
|
117
|
+
memory_entities_list: true,
|
|
118
|
+
memory_questions: true,
|
|
119
|
+
memory_last_recall: true,
|
|
120
|
+
memory_intent_debug: true,
|
|
121
|
+
memory_qmd_debug: true,
|
|
122
|
+
memory_graph_explain: true,
|
|
123
|
+
graph_snapshot: true,
|
|
124
|
+
review_list: true,
|
|
125
|
+
profiling_report: true,
|
|
126
|
+
peer_list: true,
|
|
127
|
+
peer_get: true,
|
|
128
|
+
peer_profile_get: true,
|
|
129
|
+
console_state: true,
|
|
130
|
+
dreams_status: true,
|
|
131
|
+
codegraph_list_projects: true,
|
|
132
|
+
codegraph_index_status: true,
|
|
133
|
+
codegraph_search_graph: true,
|
|
134
|
+
codegraph_trace_path: true,
|
|
135
|
+
codegraph_detect_changes: true,
|
|
136
|
+
codegraph_query_graph: true,
|
|
137
|
+
codegraph_get_schema: true,
|
|
138
|
+
codegraph_get_snippet: true,
|
|
139
|
+
codegraph_get_architecture: true,
|
|
140
|
+
codegraph_search_code: true,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* MCP protocol versions this server understands, ordered newest → oldest.
|
|
145
|
+
* Exported so the HTTP transport can validate the `MCP-Protocol-Version`
|
|
146
|
+
* header and reject requests advertising an unknown version.
|
|
147
|
+
*/
|
|
148
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [
|
|
149
|
+
"2025-06-18",
|
|
150
|
+
"2025-03-26",
|
|
151
|
+
"2024-11-05",
|
|
152
|
+
];
|
|
153
|
+
const MCP_DEFAULT_PROTOCOL_VERSION: string = MCP_SUPPORTED_PROTOCOL_VERSIONS[0] ?? "2025-06-18";
|
|
82
154
|
const LEGACY_MCP_PREFIX = "engram.";
|
|
83
155
|
const CANONICAL_MCP_PREFIX = "remnic.";
|
|
84
156
|
|
|
@@ -87,13 +159,27 @@ function toCanonicalToolName(name: string): string {
|
|
|
87
159
|
? `${CANONICAL_MCP_PREFIX}${name.slice(LEGACY_MCP_PREFIX.length)}`
|
|
88
160
|
: name;
|
|
89
161
|
}
|
|
90
|
-
|
|
91
162
|
function toLegacyToolName(name: string): string {
|
|
92
163
|
return name.startsWith(CANONICAL_MCP_PREFIX)
|
|
93
164
|
? `${LEGACY_MCP_PREFIX}${name.slice(CANONICAL_MCP_PREFIX.length)}`
|
|
94
165
|
: name;
|
|
95
166
|
}
|
|
96
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Suffix-based allowlist matcher. Returns true for tools whose canonical
|
|
170
|
+
* suffix (after stripping `remnic.` or `engram.`) is in
|
|
171
|
+
* {@link MCP_READ_ONLY_TOOL_SUFFIXES}. Unprefixed names are treated as
|
|
172
|
+
* unannotated.
|
|
173
|
+
*/
|
|
174
|
+
function isReadOnlyToolName(name: string): boolean {
|
|
175
|
+
for (const prefix of [CANONICAL_MCP_PREFIX, LEGACY_MCP_PREFIX]) {
|
|
176
|
+
if (name.startsWith(prefix)) {
|
|
177
|
+
return MCP_READ_ONLY_TOOL_SUFFIXES[name.slice(prefix.length)] === true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
97
183
|
function withToolAliases(tool: McpTool, emitLegacyTools = true): McpTool[] {
|
|
98
184
|
const canonicalName = toCanonicalToolName(tool.name);
|
|
99
185
|
const canonicalTool = canonicalName === tool.name ? tool : { ...tool, name: canonicalName };
|
|
@@ -2342,6 +2428,17 @@ export class EngramMcpServer {
|
|
|
2342
2428
|
);
|
|
2343
2429
|
this.tools = [...this.tools, ...chatTools];
|
|
2344
2430
|
}
|
|
2431
|
+
// Apply `readOnlyHint` annotations to the conservative read-only
|
|
2432
|
+
// allowlist. Done as a final pass so every spread (chat, codegraph,
|
|
2433
|
+
// coding_*, correction, etc.) inherits the annotation without
|
|
2434
|
+
// scattering the same logic across every `withToolAliases` call site.
|
|
2435
|
+
// Suffix-based matching covers both the `remnic.*` and `engram.*`
|
|
2436
|
+
// naming forms emitted by `withToolAliases`.
|
|
2437
|
+
this.tools = this.tools.map((tool) =>
|
|
2438
|
+
isReadOnlyToolName(tool.name) && tool.annotations?.readOnlyHint !== true
|
|
2439
|
+
? { ...tool, annotations: { ...(tool.annotations ?? {}), readOnlyHint: true } }
|
|
2440
|
+
: tool,
|
|
2441
|
+
);
|
|
2345
2442
|
}
|
|
2346
2443
|
|
|
2347
2444
|
/** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
|
|
@@ -2369,6 +2466,24 @@ export class EngramMcpServer {
|
|
|
2369
2466
|
}
|
|
2370
2467
|
if (method === "initialize") {
|
|
2371
2468
|
const params = request.params ?? {};
|
|
2469
|
+
// MCP initialize REQUIRES params.protocolVersion (string). Reject a
|
|
2470
|
+
// missing/mistyped field with JSON-RPC invalid params instead of
|
|
2471
|
+
// silently negotiating (repo rule: never reinterpret invalid input).
|
|
2472
|
+
// An unsupported-but-well-formed version gets the spec-mandated
|
|
2473
|
+
// counter-offer below instead: the server answers with the newest
|
|
2474
|
+
// version it supports and the client decides whether to proceed.
|
|
2475
|
+
if (typeof params.protocolVersion !== "string" || params.protocolVersion.length === 0) {
|
|
2476
|
+
return {
|
|
2477
|
+
jsonrpc: "2.0",
|
|
2478
|
+
id,
|
|
2479
|
+
error: {
|
|
2480
|
+
code: -32602,
|
|
2481
|
+
message:
|
|
2482
|
+
"initialize requires params.protocolVersion (string); " +
|
|
2483
|
+
`supported versions: ${MCP_SUPPORTED_PROTOCOL_VERSIONS.join(", ")}`,
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2372
2487
|
const rawClientInfo = params.clientInfo as { name?: string; version?: string } | undefined;
|
|
2373
2488
|
// Generate a server-side session ID for this MCP session.
|
|
2374
2489
|
// The caller should send this back as Mcp-Session-Id on subsequent requests.
|
|
@@ -2391,7 +2506,9 @@ export class EngramMcpServer {
|
|
|
2391
2506
|
jsonrpc: "2.0",
|
|
2392
2507
|
id,
|
|
2393
2508
|
result: {
|
|
2394
|
-
protocolVersion:
|
|
2509
|
+
protocolVersion: MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(params.protocolVersion)
|
|
2510
|
+
? params.protocolVersion
|
|
2511
|
+
: MCP_DEFAULT_PROTOCOL_VERSION,
|
|
2395
2512
|
capabilities: {
|
|
2396
2513
|
tools: {},
|
|
2397
2514
|
resources: {},
|
package/src/index.ts
CHANGED
|
@@ -977,11 +977,14 @@ export {
|
|
|
977
977
|
// ---------------------------------------------------------------------------
|
|
978
978
|
|
|
979
979
|
export {
|
|
980
|
+
buildTokenEntry,
|
|
981
|
+
commitTokenEntry,
|
|
980
982
|
generateToken,
|
|
981
983
|
listTokens,
|
|
982
984
|
revokeToken,
|
|
983
985
|
getAllValidTokens,
|
|
984
986
|
getAllValidTokensCached,
|
|
987
|
+
getAllValidTokenEntriesCached,
|
|
985
988
|
resolveConnectorFromToken,
|
|
986
989
|
loadTokenStore,
|
|
987
990
|
saveTokenStore,
|
package/src/tokens.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildTokenEntry,
|
|
9
9
|
commitTokenEntry,
|
|
10
10
|
generateToken,
|
|
11
|
+
getAllValidTokenEntriesCached,
|
|
11
12
|
getAllValidTokensCached,
|
|
12
13
|
loadTokenStore,
|
|
13
14
|
revokeToken,
|
|
@@ -208,3 +209,60 @@ test("generateToken uses a recognizable prefix for the omp connector", async ()
|
|
|
208
209
|
await rm(dir, { recursive: true, force: true });
|
|
209
210
|
}
|
|
210
211
|
});
|
|
212
|
+
|
|
213
|
+
test("generateToken uses a recognizable prefix for the chatgpt connector", async () => {
|
|
214
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
215
|
+
try {
|
|
216
|
+
const entry = generateToken("chatgpt", tokensPath);
|
|
217
|
+
assert.equal(entry.connector, "chatgpt");
|
|
218
|
+
assert.ok(
|
|
219
|
+
entry.token.startsWith("remnic_cg_"),
|
|
220
|
+
`expected remnic_cg_ prefix, got ${entry.token}`,
|
|
221
|
+
);
|
|
222
|
+
} finally {
|
|
223
|
+
await rm(dir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("commitTokenEntry replaces the prior chatgpt entry so re-linking rotates the token", async () => {
|
|
228
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
229
|
+
try {
|
|
230
|
+
const first = generateToken("chatgpt", tokensPath);
|
|
231
|
+
commitTokenEntry(first, tokensPath);
|
|
232
|
+
const second = generateToken("chatgpt", tokensPath);
|
|
233
|
+
commitTokenEntry(second, tokensPath);
|
|
234
|
+
assert.notEqual(first.token, second.token, "re-linking must mint a new token");
|
|
235
|
+
const store = loadTokenStore(tokensPath);
|
|
236
|
+
const chatgptEntries = store.tokens.filter((t) => t.connector === "chatgpt");
|
|
237
|
+
assert.equal(chatgptEntries.length, 1, "only one chatgpt entry survives the rotate");
|
|
238
|
+
assert.equal(chatgptEntries[0]?.token, second.token, "the new token is the surviving one");
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(dir, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("entries snapshot is coherent with validation: mint and revoke take effect immediately", async () => {
|
|
245
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
246
|
+
try {
|
|
247
|
+
// Warm the cache so a stale snapshot WOULD be observable if mutation
|
|
248
|
+
// failed to invalidate it.
|
|
249
|
+
assert.deepEqual([...getAllValidTokenEntriesCached(tokensPath)], []);
|
|
250
|
+
|
|
251
|
+
// Mint-then-use: the fresh token resolves with its connector in the
|
|
252
|
+
// SAME snapshot call that validates it — no window where the token is
|
|
253
|
+
// valid but its identity is unknown.
|
|
254
|
+
const minted = generateToken("chatgpt", tokensPath);
|
|
255
|
+
const afterMint = getAllValidTokenEntriesCached(tokensPath);
|
|
256
|
+
const found = afterMint.find((entry) => entry.token === minted.token);
|
|
257
|
+
assert.ok(found, "freshly minted token must appear immediately");
|
|
258
|
+
assert.equal(found.connector, "chatgpt");
|
|
259
|
+
assert.deepEqual(getAllValidTokensCached(tokensPath), [minted.token]);
|
|
260
|
+
|
|
261
|
+
// Revoke-then-use: the token disappears from the snapshot immediately.
|
|
262
|
+
assert.equal(revokeToken("chatgpt", tokensPath), true);
|
|
263
|
+
assert.deepEqual([...getAllValidTokenEntriesCached(tokensPath)], []);
|
|
264
|
+
assert.deepEqual(getAllValidTokensCached(tokensPath), []);
|
|
265
|
+
} finally {
|
|
266
|
+
await rm(dir, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
});
|
package/src/tokens.ts
CHANGED
|
@@ -36,6 +36,7 @@ const TOKEN_PREFIXES: Record<string, string> = {
|
|
|
36
36
|
"windsurf": "remnic_ws_",
|
|
37
37
|
"amp": "remnic_am_",
|
|
38
38
|
"generic-mcp": "remnic_gm_",
|
|
39
|
+
"chatgpt": "remnic_cg_",
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
function defaultTokensPath(): string {
|
|
@@ -260,26 +261,36 @@ export function getAllValidTokens(tokensPath?: string): string[] {
|
|
|
260
261
|
return loadTokenStore(tokensPath).tokens.map((t) => t.token);
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
// Cached token
|
|
264
|
-
// Re-reads tokens.json at most once per TTL interval (default 5s).
|
|
264
|
+
// Cached token-entry snapshot to avoid synchronous disk I/O on every HTTP
|
|
265
|
+
// request. Re-reads tokens.json at most once per TTL interval (default 5s).
|
|
266
|
+
// There is deliberately exactly ONE cache: validation (token strings) and
|
|
267
|
+
// identity (connector ids) are derived from the SAME snapshot, so a token
|
|
268
|
+
// can never validate against a fresher snapshot than the one that resolves
|
|
269
|
+
// its connector. saveTokenStore() invalidates on every mutation, so a
|
|
270
|
+
// freshly minted or revoked token is coherent immediately.
|
|
265
271
|
const TOKEN_CACHE_TTL_MS = 5_000;
|
|
266
|
-
let
|
|
272
|
+
let _cachedEntries: TokenEntry[] = [];
|
|
267
273
|
let _cachedAt = 0;
|
|
268
274
|
let _cachedPath: string | undefined;
|
|
269
275
|
|
|
270
276
|
function invalidateTokenCache(): void {
|
|
271
|
-
|
|
277
|
+
_cachedEntries = [];
|
|
272
278
|
_cachedAt = 0;
|
|
273
279
|
_cachedPath = undefined;
|
|
274
280
|
}
|
|
275
281
|
|
|
276
|
-
|
|
282
|
+
/** Cached token-entry snapshot ({token, connector} pairs). */
|
|
283
|
+
export function getAllValidTokenEntriesCached(tokensPath?: string): readonly TokenEntry[] {
|
|
277
284
|
const now = Date.now();
|
|
278
|
-
if (now - _cachedAt < TOKEN_CACHE_TTL_MS && tokensPath === _cachedPath) return
|
|
279
|
-
|
|
285
|
+
if (now - _cachedAt < TOKEN_CACHE_TTL_MS && tokensPath === _cachedPath) return _cachedEntries;
|
|
286
|
+
_cachedEntries = loadTokenStore(tokensPath).tokens;
|
|
280
287
|
_cachedAt = now;
|
|
281
288
|
_cachedPath = tokensPath;
|
|
282
|
-
return
|
|
289
|
+
return _cachedEntries;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getAllValidTokensCached(tokensPath?: string): string[] {
|
|
293
|
+
return getAllValidTokenEntriesCached(tokensPath).map((entry) => entry.token);
|
|
283
294
|
}
|
|
284
295
|
|
|
285
296
|
export function resolveConnectorFromToken(token: string, tokensPath?: string): string | undefined {
|