feat(lib): bundle MCP servers from .common/lib into factory/lib/
Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for: - projects-meta-mcp v2.25.0 (TypeScript/Node) - wiki-graph v0.3.1 (TypeScript/Node) - interns-mcp v0.3.3 (Python/FastMCP) .gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc) bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
lib/wiki-graph/src/tools.ts
Normal file
166
lib/wiki-graph/src/tools.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { GraphCache } from './cache.js';
|
||||
import type { WikiGraph } from './graph.js';
|
||||
|
||||
export interface ToolResult {
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface ToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
};
|
||||
handler: (args: Record<string, unknown>) => ToolResult;
|
||||
}
|
||||
|
||||
function text(s: string, isError = false): ToolResult {
|
||||
return { content: [{ type: 'text', text: s }], isError };
|
||||
}
|
||||
|
||||
const CORPUS_PROP = {
|
||||
corpus: {
|
||||
type: 'string',
|
||||
description: 'Absolute path to the `.wiki/` corpus directory.',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the 5 v1 wiki-graph tools over a shared mtime cache.
|
||||
* Each tool takes `corpus` (abs path to a `.wiki/`); the engine parses
|
||||
* server-side, so the corpus never enters the LLM context.
|
||||
*/
|
||||
export function makeWikiGraphTools(cache: GraphCache): ToolDef[] {
|
||||
function load(args: Record<string, unknown>): WikiGraph | ToolResult {
|
||||
const corpus = args.corpus;
|
||||
if (typeof corpus !== 'string' || corpus.length === 0) {
|
||||
return text('Missing required "corpus" (absolute path to a .wiki/ directory).', true);
|
||||
}
|
||||
try {
|
||||
return cache.load(corpus);
|
||||
} catch (e) {
|
||||
return text(`Failed to read corpus "${corpus}": ${(e as Error).message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
function str(args: Record<string, unknown>, key: string): string | null {
|
||||
const v = args[key];
|
||||
return typeof v === 'string' && v.length > 0 ? v : null;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'path',
|
||||
description:
|
||||
'Shortest chain of pages between two wiki pages (undirected). The main relational query — "what connects X and Y".',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...CORPUS_PROP,
|
||||
from: { type: 'string', description: 'Start page slug.' },
|
||||
to: { type: 'string', description: 'Target page slug.' },
|
||||
},
|
||||
required: ['corpus', 'from', 'to'],
|
||||
},
|
||||
handler: (args) => {
|
||||
const g = load(args);
|
||||
if ('content' in g) return g;
|
||||
const from = str(args, 'from');
|
||||
const to = str(args, 'to');
|
||||
if (!from || !to) return text('path requires "from" and "to" slugs.', true);
|
||||
const chain = g.path(from, to);
|
||||
if (chain.length === 0) return text(`No path between "${from}" and "${to}".`);
|
||||
return text(`Path (${chain.length} nodes): ${chain.join(' → ')}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'neighbors',
|
||||
description: 'Pages reachable via outgoing links within N hops (direction respected).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...CORPUS_PROP,
|
||||
node: { type: 'string', description: 'Page slug.' },
|
||||
depth: { type: 'number', description: 'Hop radius (default 1).' },
|
||||
},
|
||||
required: ['corpus', 'node'],
|
||||
},
|
||||
handler: (args) => {
|
||||
const g = load(args);
|
||||
if ('content' in g) return g;
|
||||
const node = str(args, 'node');
|
||||
if (!node) return text('neighbors requires a "node" slug.', true);
|
||||
const depthRaw = args.depth;
|
||||
const depth =
|
||||
typeof depthRaw === 'number' && Number.isInteger(depthRaw) && depthRaw >= 1
|
||||
? depthRaw
|
||||
: 1;
|
||||
const list = g.neighbors(node, depth);
|
||||
if (list.length === 0) {
|
||||
return text(`"${node}" has no outgoing links within depth ${depth}.`);
|
||||
}
|
||||
return text(`Neighbors of "${node}" (depth ${depth}): ${list.join(', ')}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'backlinks',
|
||||
description: 'Pages that link TO this page (incoming references).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...CORPUS_PROP,
|
||||
node: { type: 'string', description: 'Page slug.' },
|
||||
},
|
||||
required: ['corpus', 'node'],
|
||||
},
|
||||
handler: (args) => {
|
||||
const g = load(args);
|
||||
if ('content' in g) return g;
|
||||
const node = str(args, 'node');
|
||||
if (!node) return text('backlinks requires a "node" slug.', true);
|
||||
const list = g.backlinks(node);
|
||||
if (list.length === 0) return text(`Nothing links to "${node}".`);
|
||||
return text(`Backlinks of "${node}": ${list.join(', ')}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'orphans',
|
||||
description: 'Wiki health: pages with no links at all, plus dangling (unresolved) link targets.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { ...CORPUS_PROP },
|
||||
required: ['corpus'],
|
||||
},
|
||||
handler: (args) => {
|
||||
const g = load(args);
|
||||
if ('content' in g) return g;
|
||||
const o = g.orphans();
|
||||
return text(
|
||||
`Orphan pages (${o.pages.length}): ${o.pages.join(', ') || '—'}\n` +
|
||||
`Dangling links (${o.dangling.length}): ${o.dangling.join(', ') || '—'}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
description: 'Corpus counters: nodes, edges, connected components, dangling links, basename collisions.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { ...CORPUS_PROP },
|
||||
required: ['corpus'],
|
||||
},
|
||||
handler: (args) => {
|
||||
const g = load(args);
|
||||
if ('content' in g) return g;
|
||||
const s = g.stats();
|
||||
return text(
|
||||
`nodes=${s.nodes} edges=${s.edges} components=${s.components} ` +
|
||||
`dangling=${s.dangling} collisions=${s.collisions}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user