@jbarket/loomai 0.5.0 → 0.6.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 (94) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/README.md +97 -0
  3. package/dist/backends/dedup.d.ts +23 -0
  4. package/dist/backends/dedup.js +100 -0
  5. package/dist/backends/embedding-runtime.js +38 -9
  6. package/dist/backends/episodes.js +1 -1
  7. package/dist/backends/fastembed.js +30 -4
  8. package/dist/backends/migrations.js +200 -0
  9. package/dist/backends/mmr.d.ts +8 -0
  10. package/dist/backends/mmr.js +18 -4
  11. package/dist/backends/people.d.ts +192 -0
  12. package/dist/backends/people.js +824 -0
  13. package/dist/backends/proposals.js +1 -1
  14. package/dist/backends/recorder-health.d.ts +34 -0
  15. package/dist/backends/recorder-health.js +117 -0
  16. package/dist/backends/salience.d.ts +39 -0
  17. package/dist/backends/salience.js +52 -4
  18. package/dist/backends/scope.d.ts +56 -0
  19. package/dist/backends/scope.js +75 -0
  20. package/dist/backends/sqlite-knowledge.d.ts +24 -4
  21. package/dist/backends/sqlite-knowledge.js +253 -91
  22. package/dist/backends/sqlite-vec.d.ts +32 -1
  23. package/dist/backends/sqlite-vec.js +394 -75
  24. package/dist/backends/types.d.ts +203 -3
  25. package/dist/categories.d.ts +2 -0
  26. package/dist/categories.js +2 -0
  27. package/dist/cli/args.d.ts +16 -1
  28. package/dist/cli/args.js +45 -1
  29. package/dist/cli/index.d.ts +7 -0
  30. package/dist/cli/index.js +57 -3
  31. package/dist/cli/knowledge.js +4 -1
  32. package/dist/cli/memory.js +2 -2
  33. package/dist/cli/person.d.ts +2 -0
  34. package/dist/cli/person.js +116 -0
  35. package/dist/cli/recall.js +2 -1
  36. package/dist/cli/remember.js +10 -0
  37. package/dist/cli/remote.d.ts +36 -0
  38. package/dist/cli/remote.js +173 -0
  39. package/dist/cli/serve.d.ts +7 -0
  40. package/dist/cli/serve.js +94 -5
  41. package/dist/cli/subcommands.d.ts +1 -1
  42. package/dist/cli/subcommands.js +1 -1
  43. package/dist/cli/update.js +30 -11
  44. package/dist/clients.js +7 -4
  45. package/dist/config.d.ts +14 -0
  46. package/dist/config.js +25 -2
  47. package/dist/index.js +22 -7
  48. package/dist/injection/harnesses.js +9 -3
  49. package/dist/install/harnesses.js +7 -1
  50. package/dist/server.d.ts +8 -1
  51. package/dist/server.js +461 -374
  52. package/dist/tools/dossier.js +5 -1
  53. package/dist/tools/eurorack-import.js +3 -0
  54. package/dist/tools/identity.d.ts +2 -0
  55. package/dist/tools/identity.js +35 -2
  56. package/dist/tools/knowledge-history.d.ts +6 -1
  57. package/dist/tools/knowledge-history.js +26 -8
  58. package/dist/tools/knowledge-maintain.d.ts +5 -0
  59. package/dist/tools/knowledge-maintain.js +92 -5
  60. package/dist/tools/knowledge-recall.d.ts +7 -1
  61. package/dist/tools/knowledge-recall.js +53 -3
  62. package/dist/tools/knowledge-verify.d.ts +11 -1
  63. package/dist/tools/knowledge-verify.js +17 -4
  64. package/dist/tools/knowledge-write.d.ts +8 -1
  65. package/dist/tools/knowledge-write.js +27 -9
  66. package/dist/tools/memory-history.d.ts +5 -0
  67. package/dist/tools/memory-history.js +52 -0
  68. package/dist/tools/memory-list.js +2 -1
  69. package/dist/tools/memory-merge.d.ts +2 -0
  70. package/dist/tools/memory-merge.js +40 -0
  71. package/dist/tools/memory-revision-restore.d.ts +5 -0
  72. package/dist/tools/memory-revision-restore.js +33 -0
  73. package/dist/tools/person.d.ts +32 -0
  74. package/dist/tools/person.js +111 -0
  75. package/dist/tools/prune.js +1 -1
  76. package/dist/tools/recall.js +7 -1
  77. package/dist/tools/remember.js +18 -2
  78. package/dist/tools/update.js +5 -1
  79. package/dist/transport/cli-endpoint.d.ts +13 -0
  80. package/dist/transport/cli-endpoint.js +97 -0
  81. package/dist/transport/guards.d.ts +14 -0
  82. package/dist/transport/guards.js +26 -0
  83. package/dist/transport/http-server.d.ts +28 -1
  84. package/dist/transport/http-server.js +156 -18
  85. package/dist/transport/knowledge-server.d.ts +11 -0
  86. package/dist/transport/knowledge-server.js +30 -0
  87. package/dist/transport/knowledge-tools.d.ts +50 -0
  88. package/dist/transport/knowledge-tools.js +290 -0
  89. package/dist/transport/mcp-app-hello.d.ts +1 -1
  90. package/dist/widgets/display-modes.html +62 -56
  91. package/dist/widgets/hello.html +62 -56
  92. package/dist/widgets/proposals.html +62 -56
  93. package/dist/widgets/top-of-mind.html +60 -54
  94. package/package.json +7 -5
@@ -13,13 +13,26 @@
13
13
  * session, so sessions are cheap routing handles over a shared contextDir.
14
14
  * WHERE it binds on the mesh is deployment (the portable-MCP ADR); THAT it
15
15
  * refuses an unsafe bind is this contract.
16
+ *
17
+ * Two auth modes, same plumbing (t-677):
18
+ * - single `token` — the full loom server (identity/memory/knowledge), one
19
+ * shared secret, no identity resolution. The original/default shape.
20
+ * - `tokens` (token → identity map) — a scoped server (e.g. the
21
+ * knowledge-only service) where the bearer itself names the caller, so
22
+ * the acting identity is derivable server-side for attribution and is
23
+ * bound to the session at open time (a later request on the same
24
+ * session presenting a DIFFERENT identity's token is refused, not
25
+ * silently re-attributed).
16
26
  */
17
27
  import { randomUUID } from 'node:crypto';
18
28
  import { createServer } from 'node:http';
19
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
- import { isInitializeRequest, EmptyResultSchema } from '@modelcontextprotocol/sdk/types.js';
29
+ import { Readable } from 'node:stream';
30
+ import { WebStandardStreamableHTTPServerTransport, isInitializeRequest } from '@modelcontextprotocol/server';
31
+ import { EmptyResultSchema } from '@modelcontextprotocol/core';
21
32
  import { createLoomServer } from '../server.js';
22
- import { assertSafeBind, checkBearer, checkPayloadSize, DEFAULT_MAX_BODY_BYTES, } from './guards.js';
33
+ import { createBackend } from '../backends/index.js';
34
+ import { handleCliBody } from './cli-endpoint.js';
35
+ import { assertSafeBind, checkBearer, checkBearerMulti, checkPayloadSize, DEFAULT_MAX_BODY_BYTES, } from './guards.js';
23
36
  /**
24
37
  * SSE keep-alive: the SDK opens the standalone GET stream but sends no
25
38
  * heartbeat, so an idle stream gets reaped by proxies/NAT (Traefik's default
@@ -38,6 +51,49 @@ function sendError(res, status, message) {
38
51
  res.writeHead(status, { 'content-type': 'application/json' });
39
52
  res.end(JSON.stringify({ error: message }));
40
53
  }
54
+ /**
55
+ * The split 2.x transport speaks Web Standard Request/Response, not node's
56
+ * (req, res) pair — bridge node:http to it. Only method/url/headers are
57
+ * needed on the way in: bodies are pre-read/parsed by collectBody and handed
58
+ * to handleRequest via options.parsedBody, so the Request itself is bodyless.
59
+ */
60
+ function nodeRequestToWebRequest(req) {
61
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
62
+ const headers = new Headers();
63
+ for (const [key, value] of Object.entries(req.headers)) {
64
+ if (value === undefined)
65
+ continue;
66
+ if (Array.isArray(value)) {
67
+ for (const v of value)
68
+ headers.append(key, v);
69
+ }
70
+ else {
71
+ headers.set(key, value);
72
+ }
73
+ }
74
+ return new Request(url, { method: req.method, headers });
75
+ }
76
+ async function sendWebResponse(res, webRes) {
77
+ const headers = {};
78
+ webRes.headers.forEach((value, key) => {
79
+ headers[key] = value;
80
+ });
81
+ res.writeHead(webRes.status, headers);
82
+ if (!webRes.body) {
83
+ res.end();
84
+ return;
85
+ }
86
+ const nodeStream = Readable.fromWeb(webRes.body);
87
+ await new Promise((resolve, reject) => {
88
+ nodeStream.pipe(res);
89
+ nodeStream.on('end', resolve);
90
+ nodeStream.on('error', reject);
91
+ res.on('close', () => {
92
+ nodeStream.destroy();
93
+ resolve();
94
+ });
95
+ });
96
+ }
41
97
  const OVERSIZED = Symbol('oversized');
42
98
  function collectBody(req, cap) {
43
99
  return new Promise((resolve, reject) => {
@@ -59,12 +115,39 @@ function collectBody(req, cap) {
59
115
  export async function startHttpServer(opts) {
60
116
  // Bind-safety (ac-lt-bind-safety): refuse an unsafe host BEFORE we open a socket.
61
117
  assertSafeBind(opts.host);
118
+ if (opts.token && opts.tokens) {
119
+ throw new Error('startHttpServer: pass either token or tokens, not both');
120
+ }
121
+ // A scoped server factory (e.g. the knowledge-only service) always needs
122
+ // to know WHO is calling for attribution — refuse to boot it open or on a
123
+ // single shared secret with no identity behind it.
124
+ if (opts.createServer && (!opts.tokens || Object.keys(opts.tokens).length === 0)) {
125
+ throw new Error('startHttpServer: a scoped createServer requires a non-empty tokens map ' +
126
+ '(per-identity bearer) — refusing to serve it unauthenticated or identity-less.');
127
+ }
62
128
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BODY_BYTES;
63
129
  const heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
64
- // One transport (+ its connected server) per live MCP session.
130
+ // Eager-migrate (t-1270): the default full server's memory backend runs
131
+ // pending schema migrations in its constructor (strict — throws loud on a
132
+ // bad migration), but that constructor previously only ran lazily, on
133
+ // whichever session's first backend-touching tool call happened to land
134
+ // first. A restart with new pending migrations looked fully up — the
135
+ // daemon bound its port and answered health probes — while memories.db
136
+ // still had the old schema underneath it, until some real caller's first
137
+ // write tripped over missing columns. Force that construction now, before
138
+ // the socket opens, so a bad or pending migration fails startup instead of
139
+ // a session three hops later. Scoped to the default server only — a scoped
140
+ // createServer (e.g. the knowledge-only service) never touches this
141
+ // backend, so forcing it there would just pay for an unused embedder load.
142
+ if (!opts.createServer)
143
+ createBackend(opts.contextDir);
144
+ const makeServer = opts.createServer ?? ((ctx) => createLoomServer(ctx));
145
+ // One transport (+ its connected server) per live MCP session, plus the
146
+ // identity it was opened for — a multi-token session is pinned to that
147
+ // identity for its lifetime (see the mismatch check in the request handler).
65
148
  const sessions = new Map();
66
- async function openSession() {
67
- const { server } = createLoomServer({ contextDir: opts.contextDir });
149
+ async function openSession(identity) {
150
+ const { server } = makeServer({ contextDir: opts.contextDir, gapsDir: opts.gapsDir, identity });
68
151
  // Fires AFTER the initialize handshake completes — getClientVersion() is
69
152
  // populated here (it is not yet at onsessioninitialized). Logging the peer
70
153
  // makes the resolved harness observable and reveals an unmapped client's
@@ -73,11 +156,11 @@ export async function startHttpServer(opts) {
73
156
  const peer = server.server.getClientVersion();
74
157
  process.stderr.write(`loom: peer connected client=${JSON.stringify(peer ?? null)}\n`);
75
158
  };
76
- const transport = new StreamableHTTPServerTransport({
159
+ const transport = new WebStandardStreamableHTTPServerTransport({
77
160
  sessionIdGenerator: () => randomUUID(),
78
161
  enableJsonResponse: true,
79
162
  onsessioninitialized: (sid) => {
80
- sessions.set(sid, transport);
163
+ sessions.set(sid, { transport, identity });
81
164
  },
82
165
  });
83
166
  // SSE keep-alive: ping the client over the standalone stream. The first tick
@@ -113,25 +196,79 @@ export async function startHttpServer(opts) {
113
196
  }
114
197
  const http = createServer((req, res) => {
115
198
  void (async () => {
116
- // Auth gate (ac-lt-auth-gate): reject unauthenticated when a token is set.
117
- const auth = checkBearer(opts.token, req.headers['authorization']);
199
+ // Auth gate (ac-lt-auth-gate): reject unauthenticated when a token (or
200
+ // token map) is configured. Multi-token mode additionally resolves
201
+ // WHICH identity made this call — the bearer itself is the identity
202
+ // claim (t-677); there is no separate login step.
203
+ const auth = opts.tokens
204
+ ? checkBearerMulti(opts.tokens, req.headers['authorization'])
205
+ : checkBearer(opts.token, req.headers['authorization']);
118
206
  if (!auth.ok)
119
207
  return sendError(res, 401, auth.error);
208
+ const identity = auth.identity ?? 'art';
120
209
  // Oversized guard (ac-lt-oversized-guard): fast path on declared length.
121
210
  const declared = Number(req.headers['content-length'] ?? 0);
122
211
  if (Number.isFinite(declared) && declared > 0 && !checkPayloadSize(declared, maxBytes).ok) {
123
212
  return sendError(res, 413, `oversized: declared ${declared} bytes exceeds cap ${maxBytes}`);
124
213
  }
214
+ // POST /cli: the CLI's remote mode (cli-endpoint.ts). Same gate and cap
215
+ // as /mcp above. Full server only: a scoped server (knowledge-only)
216
+ // can't offer the memory verbs, so it has no /cli at all.
217
+ if (new URL(req.url ?? '/', 'http://localhost').pathname === '/cli') {
218
+ if (opts.createServer)
219
+ return sendError(res, 404, 'not-found: this server has no CLI endpoint');
220
+ // Unlike /mcp (initialize handshake + mcp-session-id header), a bare
221
+ // POST could be a browser's "simple" cross-site request. So /cli needs
222
+ // a configured token, a JSON content type (forces a CORS preflight,
223
+ // which this server never grants), and no Origin header at all: the
224
+ // CLI never sends one, and a browser always does.
225
+ if (!opts.token && !opts.tokens)
226
+ return sendError(res, 403, 'forbidden: /cli requires LOOM_BEARER_TOKEN');
227
+ if (req.headers['origin'] !== undefined)
228
+ return sendError(res, 403, 'forbidden: /cli refuses browser requests');
229
+ if (req.method !== 'POST')
230
+ return sendError(res, 405, 'method-not-allowed: POST /cli');
231
+ if (!/^application\/json\b/i.test(req.headers['content-type'] ?? '')) {
232
+ return sendError(res, 415, 'unsupported-media-type: POST /cli takes application/json');
233
+ }
234
+ const body = await collectBody(req, maxBytes);
235
+ if (body === OVERSIZED)
236
+ return sendError(res, 413, `oversized: body exceeds cap ${maxBytes}`);
237
+ let parsed;
238
+ try {
239
+ parsed = JSON.parse(body);
240
+ }
241
+ catch (e) {
242
+ return sendError(res, 400, `bad-input: malformed JSON (${e.message})`);
243
+ }
244
+ const outcome = await handleCliBody(opts.contextDir, parsed);
245
+ if (!outcome.ok)
246
+ return sendError(res, 400, outcome.error);
247
+ res.writeHead(200, { 'content-type': 'application/json' });
248
+ res.end(JSON.stringify(outcome.response));
249
+ return;
250
+ }
125
251
  const sessionId = req.headers['mcp-session-id'];
252
+ // A session is pinned to the identity that opened it. Presenting a
253
+ // DIFFERENT identity's valid token against an existing session id is
254
+ // refused outright rather than silently re-attributed or routed —
255
+ // the per-identity bearer only means anything if a session can't be
256
+ // hijacked across identities by replaying its id with another token.
257
+ const existingSession = sessionId ? sessions.get(sessionId) : undefined;
258
+ if (existingSession && existingSession.identity !== identity) {
259
+ return sendError(res, 401, 'unauthorized: session belongs to a different identity');
260
+ }
126
261
  // GET = the server->client SSE stream (server push: listChanged, progress,
127
262
  // elicitation, ui:// updates). It attaches to an established session, so a
128
263
  // GET routes to that session's transport. The stream is kept alive by the
129
264
  // per-session heartbeat. A GET for a session we no longer hold is 404 (the
130
265
  // client re-initializes); a GET with no session at all is 405.
131
266
  if (req.method === 'GET' || req.method === 'DELETE') {
132
- const existing = sessionId ? sessions.get(sessionId) : undefined;
133
- if (existing) {
134
- return existing.handleRequest(req, res).catch((e) => sendError(res, 500, `internal: ${e.message}`));
267
+ if (existingSession) {
268
+ return existingSession.transport
269
+ .handleRequest(nodeRequestToWebRequest(req))
270
+ .then((webRes) => sendWebResponse(res, webRes))
271
+ .catch((e) => sendError(res, 500, `internal: ${e.message}`));
135
272
  }
136
273
  if (sessionId) {
137
274
  res.writeHead(404, { 'content-type': 'application/json' });
@@ -155,7 +292,7 @@ export async function startHttpServer(opts) {
155
292
  catch (e) {
156
293
  return sendError(res, 400, `bad-input: malformed JSON (${e.message})`);
157
294
  }
158
- let transport = sessionId ? sessions.get(sessionId) : undefined;
295
+ let transport = existingSession?.transport;
159
296
  if (!transport) {
160
297
  if (sessionId) {
161
298
  // A session id we don't have (expired / dropped). Per MCP spec, 404
@@ -168,11 +305,12 @@ export async function startHttpServer(opts) {
168
305
  if (!isInitializeRequest(parsed)) {
169
306
  return sendError(res, 400, 'bad-input: no valid session; expected an initialize request');
170
307
  }
171
- transport = await openSession();
308
+ transport = await openSession(identity);
172
309
  }
173
310
  // Dispatch totality + typed envelopes (unknown tool / bad args -> JSON-RPC
174
311
  // error) are the SDK server's job from here — never a raw throw out.
175
- await transport.handleRequest(req, res, parsed);
312
+ const webRes = await transport.handleRequest(nodeRequestToWebRequest(req), { parsedBody: parsed });
313
+ await sendWebResponse(res, webRes);
176
314
  })().catch((e) => sendError(res, 500, `internal: ${e.message}`));
177
315
  });
178
316
  await new Promise((resolve, reject) => {
@@ -185,8 +323,8 @@ export async function startHttpServer(opts) {
185
323
  port,
186
324
  host: opts.host,
187
325
  async close() {
188
- for (const t of sessions.values())
189
- await t.close().catch(() => undefined);
326
+ for (const s of sessions.values())
327
+ await s.transport.close().catch(() => undefined);
190
328
  sessions.clear();
191
329
  await new Promise((resolve) => {
192
330
  // Force-close lingering keep-alive/SSE sockets so close() can't hang.
@@ -0,0 +1,11 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ export interface KnowledgeServerConfig {
3
+ contextDir: string;
4
+ gapsDir?: string;
5
+ /** The identity this connection is serving — resolved from its bearer token. */
6
+ identity: string;
7
+ }
8
+ export interface KnowledgeServerInstance {
9
+ server: McpServer;
10
+ }
11
+ export declare function createKnowledgeOnlyServer(config: KnowledgeServerConfig): KnowledgeServerInstance;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Knowledge-only MCP server factory (t-677) — the scoped half of the
3
+ * knowledge/memory split. Registers ONLY the knowledge_* tools (via the
4
+ * same registerKnowledgeTools() the full loom server uses — see
5
+ * knowledge-tools.ts for why that sharing matters), with no identity,
6
+ * dossier, remember/recall, or any other memory-wing tool on the surface.
7
+ *
8
+ * This is what lets a second identity (e.g. Mark) read/write Art's
9
+ * knowledge store over the mesh without ever being handed memory tools:
10
+ * the MCP tool list itself is smaller, not just access-controlled.
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { McpServer } from '@modelcontextprotocol/server';
15
+ import { resolveRepoRoot } from '../config.js';
16
+ import { registerKnowledgeTools } from './knowledge-tools.js';
17
+ export function createKnowledgeOnlyServer(config) {
18
+ const { contextDir, gapsDir, identity } = config;
19
+ const pkg = JSON.parse(readFileSync(join(resolveRepoRoot(), 'package.json'), 'utf-8'));
20
+ const server = new McpServer({
21
+ name: 'loom-knowledge',
22
+ version: pkg.version,
23
+ });
24
+ // Multi-identity surface: a caller-asserted created_by that names a
25
+ // DIFFERENT identity than its own bearer resolved to is an attribution
26
+ // spoof, not a legitimate attribution (t-677 authorization finding) —
27
+ // the full/local server leaves this open since it's single-operator.
28
+ registerKnowledgeTools(server, { contextDir, gapsDir, identity, restrictCreatedByToIdentity: true });
29
+ return { server };
30
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Knowledge tool registration (t-677) — factored out of server.ts so there
3
+ * is exactly ONE implementation of "how a knowledge tool call turns into a
4
+ * backend call with identity attribution", shared by:
5
+ * - the full loom server (createLoomServer), where `identity` is the local
6
+ * one (resolveIdentityName: LOOM_IDENTITY, else the context dir's name) —
7
+ * same code path as the service below.
8
+ * - the standalone knowledge-only HTTP service (createKnowledgeOnlyServer),
9
+ * where `identity` is resolved per-connection from the per-identity
10
+ * bearer token (checkBearerMulti) and never from a client-supplied field.
11
+ *
12
+ * This is the "no separate local-file fast path" requirement from t-677:
13
+ * Art's own knowledge_write/knowledge_recall calls run through the exact
14
+ * same registration function — same backend calls, same identity threading
15
+ * — as a second identity's calls through the scoped service, so the two
16
+ * cannot silently diverge in how they read/write knowledge.db.
17
+ */
18
+ import { McpServer } from '@modelcontextprotocol/server';
19
+ export interface RegisterKnowledgeToolsOptions {
20
+ contextDir: string;
21
+ /** Knowledge-gaps dir for recall-miss logging (see knowledge-recall.ts). */
22
+ gapsDir?: string;
23
+ /**
24
+ * The identity making calls through this registration. The local identity
25
+ * (resolveIdentityName) for the main server; for the standalone knowledge-only service this is resolved per
26
+ * connection from the per-identity bearer token at session-open time —
27
+ * never from a tool argument, so a caller can't spoof attribution.
28
+ */
29
+ identity: string;
30
+ /**
31
+ * Reject a knowledge_write whose explicit `created_by` differs from
32
+ * `identity` (t-677 authorization finding). `created_by` is caller-
33
+ * asserted, free-text metadata — on the single-operator full server
34
+ * that's fine (Art legitimately attributes ours/ pages to "jonathan").
35
+ * On the multi-identity knowledge-only service it would let identity B
36
+ * claim authorship as identity A, which the per-identity bearer exists
37
+ * specifically to prevent. Default false (full-server behavior
38
+ * unchanged); createKnowledgeOnlyServer sets this true.
39
+ */
40
+ restrictCreatedByToIdentity?: boolean;
41
+ /**
42
+ * Let knowledge_maintain run the person-page misfile check, which reads
43
+ * person cards from the memory database. The full server sets this; the
44
+ * knowledge-only service must not — it never opens the memory database,
45
+ * so cards cannot leak through a shared surface (person-cards spec, c13).
46
+ */
47
+ personCards?: boolean;
48
+ }
49
+ /** Registers the full knowledge_* tool surface (11 tools) on `server`. */
50
+ export declare function registerKnowledgeTools(server: McpServer, opts: RegisterKnowledgeToolsOptions): void;