@blenau/mcp 0.1.0 → 0.2.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/index.js +284 -111
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -241,6 +241,7 @@ async function apiRequest(method, path, opts = {}) {
|
|
|
241
241
|
Authorization: `Bearer ${token}`
|
|
242
242
|
};
|
|
243
243
|
if (opts.json !== void 0) headers["Content-Type"] = "application/json";
|
|
244
|
+
if (opts.workspace) headers["X-Blenau-Workspace"] = opts.workspace;
|
|
244
245
|
let resp;
|
|
245
246
|
try {
|
|
246
247
|
resp = await fetch(url, {
|
|
@@ -263,6 +264,15 @@ async function apiRequest(method, path, opts = {}) {
|
|
|
263
264
|
return { error: "api_error", status_code: resp.status, body: text3 };
|
|
264
265
|
}
|
|
265
266
|
}
|
|
267
|
+
const echoed = resp.headers.get("X-Blenau-Workspace");
|
|
268
|
+
if (opts.workspace && echoed && echoed !== opts.workspace) {
|
|
269
|
+
return {
|
|
270
|
+
error: "workspace_mismatch",
|
|
271
|
+
message: `Write targeted ${opts.workspace} but the server resolved ${echoed}. Aborting to avoid a cross-workspace mistake.`,
|
|
272
|
+
targeted: opts.workspace,
|
|
273
|
+
resolved: echoed
|
|
274
|
+
};
|
|
275
|
+
}
|
|
266
276
|
if (resp.status === 204) return {};
|
|
267
277
|
const text2 = await resp.text();
|
|
268
278
|
if (!text2) return {};
|
|
@@ -273,12 +283,117 @@ async function apiRequest(method, path, opts = {}) {
|
|
|
273
283
|
}
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
// src/workspace.ts
|
|
287
|
+
var _active = null;
|
|
288
|
+
var _anchor = null;
|
|
289
|
+
var _cache = null;
|
|
290
|
+
var _initialized = false;
|
|
291
|
+
function isServiceMode() {
|
|
292
|
+
return detectMode() === "service";
|
|
293
|
+
}
|
|
294
|
+
async function listWorkspaces(refresh = false) {
|
|
295
|
+
if (_cache && !refresh) return _cache;
|
|
296
|
+
const res = await apiRequest("GET", "/workspaces");
|
|
297
|
+
if (!res || res.error || !Array.isArray(res.workspaces)) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
typeof res?.error === "string" ? res.error : "could not list workspaces"
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
_cache = res.workspaces;
|
|
303
|
+
return _cache;
|
|
304
|
+
}
|
|
305
|
+
async function resolve(slugOrId) {
|
|
306
|
+
const find = (ws) => ws.find((w) => w.id === slugOrId || w.slug === slugOrId) ?? null;
|
|
307
|
+
let hit = find(await listWorkspaces());
|
|
308
|
+
if (!hit) hit = find(await listWorkspaces(true));
|
|
309
|
+
return hit;
|
|
310
|
+
}
|
|
311
|
+
async function ensureInit() {
|
|
312
|
+
if (_initialized || isServiceMode()) return;
|
|
313
|
+
_initialized = true;
|
|
314
|
+
const pin = process.env.BLENAU_WORKSPACE;
|
|
315
|
+
if (pin) {
|
|
316
|
+
const w = await resolve(pin);
|
|
317
|
+
if (w) {
|
|
318
|
+
_active = w;
|
|
319
|
+
_anchor = w;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const all = await listWorkspaces();
|
|
324
|
+
const only = all[0];
|
|
325
|
+
if (all.length === 1 && only) {
|
|
326
|
+
_active = only;
|
|
327
|
+
_anchor = only;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function activeWorkspace() {
|
|
331
|
+
return _active;
|
|
332
|
+
}
|
|
333
|
+
function pinned() {
|
|
334
|
+
return !!process.env.BLENAU_WORKSPACE;
|
|
335
|
+
}
|
|
336
|
+
async function setActive(slugOrId, confirm = false) {
|
|
337
|
+
await ensureInit();
|
|
338
|
+
const w = await resolve(slugOrId);
|
|
339
|
+
if (!w) {
|
|
340
|
+
return { error: `Unknown workspace "${slugOrId}".`, workspaces: await listWorkspaces() };
|
|
341
|
+
}
|
|
342
|
+
const differsFromAnchor = !_anchor || _anchor.id !== w.id;
|
|
343
|
+
if (differsFromAnchor && !confirm) {
|
|
344
|
+
return {
|
|
345
|
+
status: "confirmation_required",
|
|
346
|
+
from: _anchor?.name ?? null,
|
|
347
|
+
to: w.name,
|
|
348
|
+
message: `Switch the write target to "${w.name}"${_anchor ? ` (currently "${_anchor.name}")` : ""}? Re-call with confirm:true. Reads don't need this \u2014 only writes go to the active workspace.`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
_active = w;
|
|
352
|
+
if (!pinned()) _anchor = w;
|
|
353
|
+
return w;
|
|
354
|
+
}
|
|
355
|
+
async function readTarget(arg) {
|
|
356
|
+
await ensureInit();
|
|
357
|
+
if (arg) {
|
|
358
|
+
const w = await resolve(arg);
|
|
359
|
+
return w?.id ?? arg;
|
|
360
|
+
}
|
|
361
|
+
return _active?.id;
|
|
362
|
+
}
|
|
363
|
+
async function writeTarget(arg, confirm = false) {
|
|
364
|
+
await ensureInit();
|
|
365
|
+
let target = _active;
|
|
366
|
+
if (arg) {
|
|
367
|
+
const w = await resolve(arg);
|
|
368
|
+
if (!w) return { error: `Unknown workspace "${arg}".`, message: "Pick a valid workspace.", workspaces: await listWorkspaces() };
|
|
369
|
+
target = w;
|
|
370
|
+
}
|
|
371
|
+
if (!target) {
|
|
372
|
+
return {
|
|
373
|
+
message: "No active workspace. Choose one with set_active_workspace before writing.",
|
|
374
|
+
workspaces: (await listWorkspaces()).filter((w) => w.can_write)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const differsFromAnchor = !_anchor || _anchor.id !== target.id;
|
|
378
|
+
if (differsFromAnchor && !confirm) {
|
|
379
|
+
return {
|
|
380
|
+
status: "confirmation_required",
|
|
381
|
+
from: _anchor?.name ?? null,
|
|
382
|
+
to: target.name,
|
|
383
|
+
message: `About to write to "${target.name}"${_anchor ? ` but the active workspace is "${_anchor.name}"` : ""}. Re-call with confirm:true, or switch deliberately with set_active_workspace.`
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return { id: target.id };
|
|
387
|
+
}
|
|
388
|
+
|
|
276
389
|
// src/server.ts
|
|
277
390
|
var INSTRUCTIONS = `You are connected to a Blenau knowledge brain \u2014 a structured, versioned, provenance-tracked knowledge base shared across agents and humans.
|
|
278
391
|
|
|
279
392
|
GOLDEN RULE \u2014 search FIRST, write LATER.
|
|
280
393
|
Before calling ingest_document or crystallize_session, ALWAYS run search_knowledge with the topic(s) of the content you plan to write. The brain already holds docs on many subjects; creating a new file when one exists fragments the knowledge and degrades future searches.
|
|
281
394
|
|
|
395
|
+
WORKSPACES \u2014 you may belong to several. Reads accept an optional \`workspace\` (slug or id) to look in a specific one; call list_workspaces() to see them. WRITES go to the ACTIVE workspace. To write in a different one, either switch deliberately with set_active_workspace (a visible checkpoint), or pass \`workspace\` + \`confirm:true\` on the write. Every write response is verified to have landed in the intended workspace \u2014 if you get a workspace_mismatch or confirmation_required, do NOT retry blindly; surface it to the user.
|
|
396
|
+
|
|
282
397
|
Routing: a workspace can have several connected GitHub repos, each with a path_prefix. Call list_repos() BEFORE writing to a specific repo and prefix your path accordingly. Edits use optimistic locking: fetch get_section right before edit_section and pass expected_version. For images/binaries prefer create_asset_upload (stream bytes via curl) over upload_asset (base64, tiny icons only). Always cite external material via the sources argument.`;
|
|
283
398
|
var sourceSchema = z.array(
|
|
284
399
|
z.object({
|
|
@@ -288,180 +403,238 @@ var sourceSchema = z.array(
|
|
|
288
403
|
retrieved_at: z.string().optional()
|
|
289
404
|
})
|
|
290
405
|
).optional();
|
|
406
|
+
var wsArg = z.string().optional().describe("Workspace slug or id to act in. Reads roam freely; writes require confirm when it differs from the active workspace.");
|
|
407
|
+
var confirmArg = z.boolean().optional().describe("Set true to confirm a write to a workspace other than the active one.");
|
|
291
408
|
function text(result) {
|
|
292
409
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
293
410
|
}
|
|
411
|
+
async function rd(method, path, opts = {}, argWorkspace) {
|
|
412
|
+
if (isServiceMode()) return apiRequest(method, path, opts);
|
|
413
|
+
const workspace = await readTarget(argWorkspace);
|
|
414
|
+
return apiRequest(method, path, { ...opts, workspace });
|
|
415
|
+
}
|
|
416
|
+
async function wr(method, path, opts = {}, argWorkspace, confirm) {
|
|
417
|
+
if (isServiceMode()) return apiRequest(method, path, opts);
|
|
418
|
+
const target = await writeTarget(argWorkspace, confirm);
|
|
419
|
+
if (!("id" in target)) return target;
|
|
420
|
+
return apiRequest(method, path, { ...opts, workspace: target.id });
|
|
421
|
+
}
|
|
294
422
|
function buildServer() {
|
|
295
423
|
const server = new McpServer(
|
|
296
|
-
{ name: "Blenau", version: "0.
|
|
424
|
+
{ name: "Blenau", version: "0.2.0" },
|
|
297
425
|
{ instructions: INSTRUCTIONS }
|
|
298
426
|
);
|
|
299
427
|
server.tool(
|
|
300
|
-
"
|
|
301
|
-
"List
|
|
428
|
+
"list_workspaces",
|
|
429
|
+
"List the workspaces you belong to, with your role and whether you can write in each, and which is currently active. Call this to discover where you can read/write. Read-only.",
|
|
302
430
|
{},
|
|
303
|
-
async () =>
|
|
431
|
+
async () => {
|
|
432
|
+
if (isServiceMode()) return text(await apiRequest("GET", "/workspaces"));
|
|
433
|
+
const workspaces = await listWorkspaces(true);
|
|
434
|
+
const active = activeWorkspace();
|
|
435
|
+
return text({ active: active?.id ?? null, active_name: active?.name ?? null, workspaces });
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
server.tool(
|
|
439
|
+
"set_active_workspace",
|
|
440
|
+
"Set the ACTIVE workspace \u2014 where writes go. Switching to a workspace other than the current one needs confirm:true (the switch is a visible checkpoint). Reads don't need this. In service mode the workspace is pinned by the token.",
|
|
441
|
+
{ workspace: z.string(), confirm: z.boolean().optional() },
|
|
442
|
+
async ({ workspace, confirm }) => {
|
|
443
|
+
if (isServiceMode()) return text({ error: "workspace pinned by service token" });
|
|
444
|
+
return text(await setActive(workspace, confirm ?? false));
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
server.tool(
|
|
448
|
+
"list_repos",
|
|
449
|
+
"List all GitHub repos connected to a workspace. Returns each repo with id, repo (org/name), label, path_prefix, installation_id, doc_count, created_at. Call this BEFORE writing to land content in a specific repo. Read-only.",
|
|
450
|
+
{ workspace: wsArg },
|
|
451
|
+
async ({ workspace }) => text(await rd("GET", "/github/repos", {}, workspace))
|
|
304
452
|
);
|
|
305
453
|
server.tool(
|
|
306
454
|
"search_knowledge",
|
|
307
455
|
"Search the knowledge base semantically. Returns ranked results with relevance scores.",
|
|
308
|
-
{ query: z.string(), top_k: z.number().int().default(10) },
|
|
309
|
-
async ({ query, top_k }) => text(await
|
|
456
|
+
{ query: z.string(), top_k: z.number().int().default(10), workspace: wsArg },
|
|
457
|
+
async ({ query, top_k, workspace }) => text(await rd("POST", "/knowledge/search", { json: { query, top_k } }, workspace))
|
|
310
458
|
);
|
|
311
459
|
server.tool(
|
|
312
460
|
"get_document",
|
|
313
461
|
"Get a document by its path, including all sections.",
|
|
314
|
-
{ path: z.string() },
|
|
315
|
-
async ({ path }) => text(await
|
|
462
|
+
{ path: z.string(), workspace: wsArg },
|
|
463
|
+
async ({ path, workspace }) => text(await rd("GET", `/knowledge/documents/by-path/${path}`, {}, workspace))
|
|
316
464
|
);
|
|
317
465
|
server.tool(
|
|
318
466
|
"get_document_structure",
|
|
319
467
|
"Get the outline (headings tree) of a document without full content.",
|
|
320
|
-
{ path: z.string() },
|
|
321
|
-
async ({ path }) => text(await
|
|
468
|
+
{ path: z.string(), workspace: wsArg },
|
|
469
|
+
async ({ path, workspace }) => text(await rd("GET", `/knowledge/structure/${path}`, {}, workspace))
|
|
322
470
|
);
|
|
323
471
|
server.tool(
|
|
324
472
|
"get_section",
|
|
325
473
|
"Get a specific section by path and heading. Returns content + version hash for editing.",
|
|
326
|
-
{ path: z.string(), heading: z.string() },
|
|
327
|
-
async ({ path, heading }) => text(await
|
|
474
|
+
{ path: z.string(), heading: z.string(), workspace: wsArg },
|
|
475
|
+
async ({ path, heading, workspace }) => text(await rd("GET", `/knowledge/section/${path}`, { params: { heading } }, workspace))
|
|
476
|
+
);
|
|
477
|
+
server.tool(
|
|
478
|
+
"list_documents",
|
|
479
|
+
"List all documents in the knowledge base with their status.",
|
|
480
|
+
{ workspace: wsArg },
|
|
481
|
+
async ({ workspace }) => text(await rd("GET", "/knowledge/documents", {}, workspace))
|
|
482
|
+
);
|
|
483
|
+
server.tool(
|
|
484
|
+
"list_assets",
|
|
485
|
+
"List the image references in a doc and whether each resolves in GitHub. Returns { doc_path, doc_id, assets: [...] }.",
|
|
486
|
+
{ doc_path: z.string(), workspace: wsArg },
|
|
487
|
+
async ({ doc_path, workspace }) => text(await rd("GET", "/assets/list", { params: { doc_path } }, workspace))
|
|
488
|
+
);
|
|
489
|
+
server.tool(
|
|
490
|
+
"verify_asset",
|
|
491
|
+
"Check that ONE asset reference resolves (exists in GitHub \u2192 will render). relative_path is the path exactly as written in the markdown. Returns { resolves, asset_path, raw_url }.",
|
|
492
|
+
{ doc_path: z.string(), relative_path: z.string(), workspace: wsArg },
|
|
493
|
+
async ({ doc_path, relative_path, workspace }) => text(await rd("GET", "/assets/resolve", { params: { doc_path, relative_path } }, workspace))
|
|
494
|
+
);
|
|
495
|
+
server.tool(
|
|
496
|
+
"audit_links",
|
|
497
|
+
"Audit the knowledge base for orphan documents, one-way links, unlinked-but-similar pairs, docs missing citations, and recurring concepts lacking a doc. Read-only.",
|
|
498
|
+
{ workspace: wsArg },
|
|
499
|
+
async ({ workspace }) => text(await rd("GET", "/github/audit-links", {}, workspace))
|
|
500
|
+
);
|
|
501
|
+
server.tool(
|
|
502
|
+
"suggest_crosslinks",
|
|
503
|
+
"Suggest markdown cross-links to add in a specific document. Returns the top 5 semantically-similar sections from other documents not currently linked, with ready-to-paste snippets. Read-only.",
|
|
504
|
+
{ path: z.string(), workspace: wsArg },
|
|
505
|
+
async ({ path, workspace }) => {
|
|
506
|
+
const doc = await rd("GET", `/knowledge/documents/by-path/${path}`, {}, workspace);
|
|
507
|
+
if (!doc || typeof doc !== "object" || "error" in doc) return text(doc ?? { error: "lookup_failed" });
|
|
508
|
+
const docId = doc.id ?? doc.document_id;
|
|
509
|
+
if (!docId) return text({ error: `Document not found: ${path}` });
|
|
510
|
+
const out = await rd("GET", `/github/suggest-crosslinks/${docId}`, {}, workspace);
|
|
511
|
+
if (out && typeof out === "object" && "suggestions" in out && !("document_path" in out)) {
|
|
512
|
+
out.document_path = path;
|
|
513
|
+
}
|
|
514
|
+
return text(out);
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
server.tool(
|
|
518
|
+
"get_audit_log",
|
|
519
|
+
"Get the audit log for a workspace. Shows who did what and when.",
|
|
520
|
+
{ limit: z.number().int().default(50), event_type: z.string().optional(), workspace: wsArg },
|
|
521
|
+
async ({ limit, event_type, workspace }) => text(
|
|
522
|
+
await rd(
|
|
523
|
+
"GET",
|
|
524
|
+
"/knowledge/audit-log",
|
|
525
|
+
{ params: { limit, ...event_type ? { event_type } : {} } },
|
|
526
|
+
workspace
|
|
527
|
+
)
|
|
528
|
+
)
|
|
328
529
|
);
|
|
329
530
|
server.tool(
|
|
330
531
|
"ingest_document",
|
|
331
|
-
"Ingest a document into the knowledge base
|
|
532
|
+
"Ingest a document into the knowledge base (parse, chunk, embed, index). Routing: path matches a connected repo by longest path_prefix; call list_repos() first. auto_link rewrites bare mentions into links. sources: external material appended as a `## Fuentes` section. Writes go to the active workspace.",
|
|
332
533
|
{
|
|
333
534
|
path: z.string(),
|
|
334
535
|
title: z.string(),
|
|
335
536
|
content: z.string(),
|
|
336
537
|
source_type: z.string().default("manual"),
|
|
337
538
|
auto_link: z.boolean().default(false),
|
|
338
|
-
sources: sourceSchema
|
|
539
|
+
sources: sourceSchema,
|
|
540
|
+
workspace: wsArg,
|
|
541
|
+
confirm: confirmArg
|
|
339
542
|
},
|
|
340
|
-
async ({ path, title, content, source_type, auto_link, sources }) => text(
|
|
341
|
-
await
|
|
342
|
-
|
|
343
|
-
|
|
543
|
+
async ({ path, title, content, source_type, auto_link, sources, workspace, confirm }) => text(
|
|
544
|
+
await wr(
|
|
545
|
+
"POST",
|
|
546
|
+
"/knowledge/ingest-enhanced",
|
|
547
|
+
{ json: { path, title, content, source_type, auto_link, sources: sources ?? null } },
|
|
548
|
+
workspace,
|
|
549
|
+
confirm
|
|
550
|
+
)
|
|
344
551
|
)
|
|
345
552
|
);
|
|
346
553
|
server.tool(
|
|
347
554
|
"crystallize_session",
|
|
348
|
-
"Crystallize
|
|
555
|
+
"Crystallize a session into a single structured document. Prefer smart_crystallize for multi-topic sessions. output_path is the folder (default docs/crystallized). Writes go to the active workspace.",
|
|
349
556
|
{
|
|
350
557
|
session_context: z.string(),
|
|
351
558
|
title: z.string().default("Crystallized Knowledge"),
|
|
352
559
|
output_path: z.string().default("docs/crystallized"),
|
|
353
|
-
sources: sourceSchema
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
await apiRequest("POST", "/knowledge/crystallize-enhanced", {
|
|
357
|
-
json: { session_context, title, output_path, sources: sources ?? null }
|
|
358
|
-
})
|
|
359
|
-
)
|
|
360
|
-
);
|
|
361
|
-
server.tool(
|
|
362
|
-
"create_asset_upload",
|
|
363
|
-
'PREFERRED way to attach an image / PDF / binary to a doc. Returns a short-lived upload_url; transfer the LOCAL file with one curl command so bytes never pass through you (e.g. `curl -F file=@/path/img.webp "<upload_url>"`; add -F insert_heading / -F insert_position to embed). Asset names are predictable (assets/<filename>); a content hash is appended only on a real collision. Embedding is idempotent. Size limit 1 MB \u2014 compress locally first if larger. Params: doc_path (routes the commit), filename, content_type (optional MIME).',
|
|
364
|
-
{ doc_path: z.string(), filename: z.string(), content_type: z.string().default("") },
|
|
365
|
-
async ({ doc_path, filename, content_type }) => text(
|
|
366
|
-
await apiRequest("POST", "/assets/upload-url", {
|
|
367
|
-
params: { doc_path, filename, content_type }
|
|
368
|
-
})
|
|
369
|
-
)
|
|
370
|
-
);
|
|
371
|
-
server.tool(
|
|
372
|
-
"list_assets",
|
|
373
|
-
"List the image references in a doc and whether each resolves in GitHub. Returns { doc_path, doc_id, assets: [...] }. resolves:false means the markdown points at a path that isn't committed (the usual broken-image cause).",
|
|
374
|
-
{ doc_path: z.string() },
|
|
375
|
-
async ({ doc_path }) => text(await apiRequest("GET", "/assets/list", { params: { doc_path } }))
|
|
376
|
-
);
|
|
377
|
-
server.tool(
|
|
378
|
-
"verify_asset",
|
|
379
|
-
"Check that ONE asset reference resolves (exists in GitHub \u2192 will render). relative_path is the path exactly as written in the markdown (e.g. assets/diagram.webp). Returns { resolves, asset_path, raw_url }.",
|
|
380
|
-
{ doc_path: z.string(), relative_path: z.string() },
|
|
381
|
-
async ({ doc_path, relative_path }) => text(await apiRequest("GET", "/assets/resolve", { params: { doc_path, relative_path } }))
|
|
382
|
-
);
|
|
383
|
-
server.tool(
|
|
384
|
-
"delete_asset",
|
|
385
|
-
"Delete an asset FILE from the repo. Does NOT rewrite document references \u2014 if the doc still links it, remove that markdown separately via edit_section. relative_path is the path as written in the markdown. Returns { deleted, asset_path }.",
|
|
386
|
-
{ doc_path: z.string(), relative_path: z.string() },
|
|
387
|
-
async ({ doc_path, relative_path }) => text(await apiRequest("DELETE", "/assets/file", { params: { doc_path, relative_path } }))
|
|
388
|
-
);
|
|
389
|
-
server.tool(
|
|
390
|
-
"upload_asset",
|
|
391
|
-
"LEGACY base64 upload \u2014 TINY assets only (<= 64 KB, e.g. a small SVG icon). For photos/screenshots or anything larger, use create_asset_upload instead. Commits the asset under an assets/ subdirectory of the doc's repo and returns a markdown snippet with a RELATIVE path. Params: doc_path, filename, content_base64 (data-URL prefix accepted), alt_text.",
|
|
392
|
-
{
|
|
393
|
-
doc_path: z.string(),
|
|
394
|
-
filename: z.string(),
|
|
395
|
-
content_base64: z.string(),
|
|
396
|
-
alt_text: z.string().default("")
|
|
560
|
+
sources: sourceSchema,
|
|
561
|
+
workspace: wsArg,
|
|
562
|
+
confirm: confirmArg
|
|
397
563
|
},
|
|
398
|
-
async ({
|
|
399
|
-
await
|
|
400
|
-
|
|
401
|
-
|
|
564
|
+
async ({ session_context, title, output_path, sources, workspace, confirm }) => text(
|
|
565
|
+
await wr(
|
|
566
|
+
"POST",
|
|
567
|
+
"/knowledge/crystallize-enhanced",
|
|
568
|
+
{ json: { session_context, title, output_path, sources: sources ?? null } },
|
|
569
|
+
workspace,
|
|
570
|
+
confirm
|
|
571
|
+
)
|
|
402
572
|
)
|
|
403
573
|
);
|
|
404
574
|
server.tool(
|
|
405
575
|
"smart_crystallize",
|
|
406
|
-
"Intelligently crystallize a session
|
|
407
|
-
{ session_context: z.string(), repo_hint: z.string().optional() },
|
|
408
|
-
async ({ session_context, repo_hint }) => text(
|
|
409
|
-
await
|
|
410
|
-
|
|
411
|
-
|
|
576
|
+
"Intelligently crystallize a session: split into topical blocks, search existing docs per topic, route each to edit/append/create. Opens one PR per affected repo. Writes go to the active workspace.",
|
|
577
|
+
{ session_context: z.string(), repo_hint: z.string().optional(), workspace: wsArg, confirm: confirmArg },
|
|
578
|
+
async ({ session_context, repo_hint, workspace, confirm }) => text(
|
|
579
|
+
await wr(
|
|
580
|
+
"POST",
|
|
581
|
+
"/knowledge/smart-crystallize",
|
|
582
|
+
{ json: { session_context, repo_hint: repo_hint ?? null } },
|
|
583
|
+
workspace,
|
|
584
|
+
confirm
|
|
585
|
+
)
|
|
412
586
|
)
|
|
413
587
|
);
|
|
414
|
-
server.tool(
|
|
415
|
-
"list_documents",
|
|
416
|
-
"List all documents in the knowledge base with their status.",
|
|
417
|
-
{},
|
|
418
|
-
async () => text(await apiRequest("GET", "/knowledge/documents"))
|
|
419
|
-
);
|
|
420
588
|
server.tool(
|
|
421
589
|
"edit_section",
|
|
422
|
-
"Edit a section with optimistic locking. Pass the version from get_section to prevent conflicts.",
|
|
590
|
+
"Edit a section with optimistic locking. Pass the version from get_section to prevent conflicts. Writes go to the active workspace.",
|
|
423
591
|
{
|
|
424
592
|
path: z.string(),
|
|
425
593
|
heading: z.string(),
|
|
426
594
|
new_content: z.string(),
|
|
427
|
-
expected_version: z.string()
|
|
595
|
+
expected_version: z.string(),
|
|
596
|
+
workspace: wsArg,
|
|
597
|
+
confirm: confirmArg
|
|
428
598
|
},
|
|
429
|
-
async ({ path, heading, new_content, expected_version }) => text(
|
|
430
|
-
await
|
|
431
|
-
|
|
432
|
-
|
|
599
|
+
async ({ path, heading, new_content, expected_version, workspace, confirm }) => text(
|
|
600
|
+
await wr(
|
|
601
|
+
"POST",
|
|
602
|
+
"/knowledge/edit-section",
|
|
603
|
+
{ json: { path, heading, new_content, expected_version } },
|
|
604
|
+
workspace,
|
|
605
|
+
confirm
|
|
606
|
+
)
|
|
433
607
|
)
|
|
434
608
|
);
|
|
435
609
|
server.tool(
|
|
436
|
-
"
|
|
437
|
-
|
|
438
|
-
{},
|
|
439
|
-
async () => text(
|
|
610
|
+
"create_asset_upload",
|
|
611
|
+
'PREFERRED way to attach an image/PDF/binary. Returns a short-lived upload_url; transfer the LOCAL file with one curl command (e.g. `curl -F file=@/path/img.webp "<upload_url>"`). Size limit 1 MB. The workspace is sealed into the upload_url at mint. Writes go to the active workspace.',
|
|
612
|
+
{ doc_path: z.string(), filename: z.string(), content_type: z.string().default(""), workspace: wsArg, confirm: confirmArg },
|
|
613
|
+
async ({ doc_path, filename, content_type, workspace, confirm }) => text(
|
|
614
|
+
await wr("POST", "/assets/upload-url", { params: { doc_path, filename, content_type } }, workspace, confirm)
|
|
615
|
+
)
|
|
440
616
|
);
|
|
441
617
|
server.tool(
|
|
442
|
-
"
|
|
443
|
-
"
|
|
444
|
-
{
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
}
|
|
618
|
+
"upload_asset",
|
|
619
|
+
"LEGACY base64 upload \u2014 TINY assets only (<= 64 KB). For anything larger use create_asset_upload. Writes go to the active workspace.",
|
|
620
|
+
{
|
|
621
|
+
doc_path: z.string(),
|
|
622
|
+
filename: z.string(),
|
|
623
|
+
content_base64: z.string(),
|
|
624
|
+
alt_text: z.string().default(""),
|
|
625
|
+
workspace: wsArg,
|
|
626
|
+
confirm: confirmArg
|
|
627
|
+
},
|
|
628
|
+
async ({ doc_path, filename, content_base64, alt_text, workspace, confirm }) => text(
|
|
629
|
+
await wr("POST", "/assets/upload", { json: { doc_path, filename, content_base64, alt_text } }, workspace, confirm)
|
|
630
|
+
)
|
|
456
631
|
);
|
|
457
632
|
server.tool(
|
|
458
|
-
"
|
|
459
|
-
"
|
|
460
|
-
{
|
|
461
|
-
async ({
|
|
462
|
-
await
|
|
463
|
-
params: { limit, ...event_type ? { event_type } : {} }
|
|
464
|
-
})
|
|
633
|
+
"delete_asset",
|
|
634
|
+
"Delete an asset FILE from the repo. Does NOT rewrite document references. relative_path is the path as written in the markdown. Writes go to the active workspace.",
|
|
635
|
+
{ doc_path: z.string(), relative_path: z.string(), workspace: wsArg, confirm: confirmArg },
|
|
636
|
+
async ({ doc_path, relative_path, workspace, confirm }) => text(
|
|
637
|
+
await wr("DELETE", "/assets/file", { params: { doc_path, relative_path } }, workspace, confirm)
|
|
465
638
|
)
|
|
466
639
|
);
|
|
467
640
|
return server;
|
|
@@ -473,7 +646,7 @@ async function runStdioServer() {
|
|
|
473
646
|
}
|
|
474
647
|
|
|
475
648
|
// src/index.ts
|
|
476
|
-
var VERSION = "0.
|
|
649
|
+
var VERSION = "0.2.0";
|
|
477
650
|
function rejectInService(action) {
|
|
478
651
|
if (detectMode() === "service") {
|
|
479
652
|
process.stderr.write(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blenau/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Blenau MCP server + CLI. Connects agents (Claude Code, Cursor, …) to the Blenau knowledge brain over stdio, with credentials in the OS keychain — never a token in a config file or URL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"author": "Blenau (Ganemo)",
|
|
15
15
|
"bin": {
|
|
16
|
-
"blenau": "dist/index.js"
|
|
16
|
+
"blenau-mcp": "dist/index.js"
|
|
17
17
|
},
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"files": [
|