@aabadin/project-memory-context 0.1.5 → 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/cli/context.mjs CHANGED
@@ -1,12 +1,211 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
3
- import { dirname, resolve } from 'node:path';
2
+ import { access } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- const SCRIPT_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'project-context.mjs');
6
+ import { readJsonArtifact } from '../src/artifacts.mjs';
7
+ import { createQueryEngine, focusToEdgeTypes } from '../src/retrieval/query-engine.mjs';
8
+ import { resolveTarget } from '../src/retrieval/target-resolver.mjs';
9
+ import { renderTargetContext } from '../src/retrieval/context-renderer-v1.mjs';
7
10
 
8
- function printHelp() {
9
- console.log('Usage: pmc context [project-root] [--refresh]');
11
+ async function fileExists(filePath) {
12
+ try {
13
+ await access(filePath);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ export async function findProjectRoot(startDir = process.cwd()) {
21
+ let currentDir = resolve(startDir);
22
+
23
+ while (true) {
24
+ const installPath = join(currentDir, '.planning', 'project-memory-context', 'install.json');
25
+ if (await fileExists(installPath)) {
26
+ return currentDir;
27
+ }
28
+
29
+ const parentDir = dirname(currentDir);
30
+ if (parentDir === currentDir) {
31
+ return null;
32
+ }
33
+
34
+ currentDir = parentDir;
35
+ }
36
+ }
37
+
38
+ export function parseArgs(args) {
39
+ const DEPTH_VALUES = ['compact', 'extended', 'deep', 'disk'];
40
+ const FOCUS_VALUES = ['dependencies', 'callers', 'containment', 'impact', 'all'];
41
+ const EXPLICIT_MODES = ['symbol', 'file', 'query'];
42
+
43
+ let explicitMode = null;
44
+ let target = undefined;
45
+ let depth = 'compact';
46
+ let focus = 'all';
47
+ let refresh = false;
48
+ let help = false;
49
+ const positional = [];
50
+
51
+ for (const arg of args) {
52
+ if (arg === '--help' || arg === '-h') {
53
+ help = true;
54
+ continue;
55
+ }
56
+
57
+ if (arg === '--refresh') {
58
+ refresh = true;
59
+ continue;
60
+ }
61
+
62
+ if (EXPLICIT_MODES.includes(arg) && explicitMode === null && positional.length === 0) {
63
+ explicitMode = arg;
64
+ continue;
65
+ }
66
+
67
+ if (DEPTH_VALUES.includes(arg)) {
68
+ depth = arg;
69
+ continue;
70
+ }
71
+
72
+ if (FOCUS_VALUES.includes(arg)) {
73
+ focus = arg;
74
+ continue;
75
+ }
76
+
77
+ positional.push(arg);
78
+ }
79
+
80
+ if (positional.length > 0) {
81
+ target = positional[0];
82
+ }
83
+
84
+ return { explicitMode, target, depth, focus, refresh, help };
85
+ }
86
+
87
+ export async function loadArtifacts(projectRoot) {
88
+ const pmcRoot = join(projectRoot, '.planning', 'project-memory-context');
89
+ try {
90
+ const [graph, symbolIndex, worklist] = await Promise.all([
91
+ readJsonArtifact(join(pmcRoot, 'graph', 'graph.json'), { nodes: [], links: [] }),
92
+ readJsonArtifact(join(pmcRoot, 'enrichment', 'symbol-index.json'), {}),
93
+ readJsonArtifact(join(pmcRoot, 'enrichment', 'worklist.json'), []),
94
+ ]);
95
+ return { graph, symbolIndex, worklist };
96
+ } catch (error) {
97
+ throw new Error(`Failed to load PMC artifacts from ${pmcRoot}: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ function groupEdges(edges, edgeTypes) {
102
+ const byKind = new Map();
103
+ for (const edge of edges) {
104
+ if (!edgeTypes.includes(edge.relation)) continue;
105
+ const kind = edge.relation;
106
+ const arr = byKind.get(kind);
107
+ if (arr) {
108
+ arr.push(edge.target);
109
+ } else {
110
+ byKind.set(kind, [edge.target]);
111
+ }
112
+ }
113
+
114
+ const result = [];
115
+ for (const [kind, items] of byKind) {
116
+ result.push({ kind, items });
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ export function buildRenderInput(engine, resolved, { depth, focus }) {
123
+ const edgeTypes = focusToEdgeTypes(focus);
124
+ let summary = [];
125
+ let target = {};
126
+ let relevant = [];
127
+ let relations = [];
128
+ let nextReads = [];
129
+
130
+ switch (resolved.mode) {
131
+ case 'symbol': {
132
+ const ctx = engine.querySymbolContext({ symbolKey: resolved.symbolKey, depth });
133
+ target = { mode: 'symbol', name: ctx.target?.name, filePath: ctx.target?.filePath };
134
+ summary = [`Symbol: ${ctx.target?.name || resolved.target} (${ctx.target?.kind || 'unknown'})`];
135
+ relevant = (ctx.neighbors || []).map((n) => ({
136
+ label: n.name || n.label || 'unknown',
137
+ filePath: n.filePath || undefined,
138
+ }));
139
+ relations = groupEdges(ctx.edges || [], edgeTypes);
140
+ nextReads = relevant.map((r) => r.filePath).filter(Boolean);
141
+ break;
142
+ }
143
+ case 'symbol-ambiguous': {
144
+ target = { mode: 'symbol-ambiguous', name: resolved.target };
145
+ summary = [`Multiple symbols match "${resolved.target}".`];
146
+ relevant = (resolved.symbolKeys || []).map((sk) => {
147
+ const parts = sk.split('|');
148
+ return {
149
+ label: parts.length >= 6 ? parts[parts.length - 2] : sk,
150
+ filePath: parts.length >= 2 ? parts[1] : undefined,
151
+ };
152
+ });
153
+ break;
154
+ }
155
+ case 'file': {
156
+ const ctx = engine.queryFileContext({ filePath: resolved.target, depth });
157
+ target = { mode: 'file', filePath: resolved.target };
158
+ summary = ctx.symbols && ctx.symbols.length > 0
159
+ ? ctx.symbols.map((s) => `${s.name || 'unknown'} (${s.kind || 'symbol'})`)
160
+ : [`File: ${resolved.target}`];
161
+ relevant = (ctx.neighbors || []).map((n) => ({
162
+ label: n.name || n.label || 'unknown',
163
+ filePath: n.filePath || undefined,
164
+ }));
165
+ relations = groupEdges(ctx.edges || [], edgeTypes);
166
+ nextReads = relevant.map((r) => r.filePath).filter(Boolean);
167
+ break;
168
+ }
169
+ case 'query':
170
+ target = { mode: 'query', value: resolved.target };
171
+ summary = [`Query: ${resolved.target} (no structural context available)`];
172
+ break;
173
+ case 'symbol-missing':
174
+ target = { mode: 'symbol-missing', name: resolved.target };
175
+ summary = [`Symbol "${resolved.target}" not found in project index.`];
176
+ break;
177
+ default:
178
+ target = { mode: resolved.mode || 'unknown', value: resolved.target };
179
+ summary = [`Unrecognized target mode: ${resolved.mode}`];
180
+ break;
181
+ }
182
+
183
+ return {
184
+ summary,
185
+ target,
186
+ relevant,
187
+ relations,
188
+ nextReads,
189
+ metadata: { depth, focus },
190
+ };
191
+ }
192
+
193
+ export async function runTargetContext({ projectRoot, target, explicitMode, depth, focus }) {
194
+ const artifacts = await loadArtifacts(projectRoot);
195
+ const engine = createQueryEngine({
196
+ graph: artifacts.graph,
197
+ symbolIndex: artifacts.symbolIndex,
198
+ worklist: artifacts.worklist,
199
+ enrichmentDir: join(projectRoot, '.planning', 'project-memory-context', 'enrichment'),
200
+ projectSlug: 'project',
201
+ });
202
+
203
+ const resolved = resolveTarget({ engine, explicitMode, target });
204
+
205
+ const input = buildRenderInput(engine, resolved, { depth, focus });
206
+ const output = renderTargetContext(input);
207
+
208
+ return { output, resolved, input };
10
209
  }
11
210
 
12
211
  export async function runProjectContext(projectRoot = process.cwd(), refresh = false) {
@@ -14,27 +213,104 @@ export async function runProjectContext(projectRoot = process.cwd(), refresh = f
14
213
  if (typeof mod.runProjectContextCli === 'function') {
15
214
  return mod.runProjectContextCli(projectRoot, { refresh });
16
215
  }
216
+
17
217
  return null;
18
218
  }
19
219
 
220
+ function printHelp() {
221
+ console.log('Usage: pmc context [options] [<target>]');
222
+ console.log(' pmc context {symbol|file|query} <target> [depth] [focus]');
223
+ console.log('');
224
+ console.log('Get structural context about symbols, files, or free-text queries');
225
+ console.log('from the PMC project graph.');
226
+ console.log('');
227
+ console.log('Modes:');
228
+ console.log(' symbol Resolve a symbol by name');
229
+ console.log(' file Query context for a file path');
230
+ console.log(' query Free-text query (structural only)');
231
+ console.log('');
232
+ console.log('Options:');
233
+ console.log(' depth compact (default), extended, deep, disk');
234
+ console.log(' focus all (default), dependencies, callers, containment, impact');
235
+ console.log(' --refresh Run project-context detection and materialization');
236
+ console.log(' --help, -h Show this help');
237
+ console.log('');
238
+ console.log('Examples:');
239
+ console.log(' pmc context createQueryEngine');
240
+ console.log(' pmc context symbol MyFunc extended dependencies');
241
+ console.log(' pmc context file src/auth.ts deep callers');
242
+ console.log(' pmc context query "how auth works"');
243
+ console.log(' pmc context . --refresh');
244
+ }
245
+
20
246
  export async function main(args = process.argv.slice(2)) {
21
- if (args.includes('--help') || args.includes('-h')) {
247
+ const parsed = parseArgs(args);
248
+
249
+ if (parsed.help) {
22
250
  printHelp();
23
251
  return 0;
24
252
  }
25
253
 
26
- return await new Promise((resolvePromise, rejectPromise) => {
27
- const child = spawn(process.execPath, [SCRIPT_PATH, ...args], { stdio: 'inherit' });
28
- child.once('error', rejectPromise);
29
- child.once('exit', (code, signal) => {
30
- if (signal) {
31
- rejectPromise(new Error(`context exited from signal ${signal}`));
32
- return;
254
+ if (parsed.refresh) {
255
+ let projectRoot;
256
+
257
+ if (parsed.target && parsed.target !== '.') {
258
+ const looksValid = /^[A-Za-z]:[\\/]/.test(parsed.target)
259
+ || /^[\\/]/.test(parsed.target)
260
+ || /^\.{2}[\\/]?/.test(parsed.target);
261
+
262
+ if (looksValid) {
263
+ projectRoot = resolve(parsed.target);
264
+ } else {
265
+ console.error(
266
+ `[context] --refresh does not accept a non-path target "${parsed.target}".`,
267
+ 'Use . --refresh to refresh from cwd, or omit the target entirely.',
268
+ );
269
+ return 1;
33
270
  }
271
+ } else {
272
+ projectRoot = await findProjectRoot();
273
+ }
274
+
275
+ if (!projectRoot) {
276
+ console.error('[context] Not inside a PMC-enabled project.');
277
+ return 1;
278
+ }
34
279
 
35
- resolvePromise(code ?? 0);
36
- });
280
+ try {
281
+ return await runProjectContext(projectRoot, true);
282
+ } catch (error) {
283
+ console.error(`[context] Refresh failed: ${error.message}`);
284
+ return 1;
285
+ }
286
+ }
287
+
288
+ if (!parsed.target) {
289
+ if (parsed.explicitMode) {
290
+ console.error(`[context] Missing target for ${parsed.explicitMode} mode.`);
291
+ return 1;
292
+ }
293
+
294
+ printHelp();
295
+ return 0;
296
+ }
297
+
298
+ const projectRoot = await findProjectRoot();
299
+ if (!projectRoot) {
300
+ console.error('[context] Not inside a PMC-enabled project (no install.json found).');
301
+ return 1;
302
+ }
303
+
304
+ const { output } = await runTargetContext({
305
+ projectRoot,
306
+ target: parsed.target,
307
+ explicitMode: parsed.explicitMode,
308
+ depth: parsed.depth,
309
+ focus: parsed.focus,
37
310
  });
311
+
312
+ console.log(output);
313
+ return 0;
38
314
  }
39
315
 
40
316
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -59,11 +59,16 @@ function copyTemplatesDir(srcTemplates, dstTemplates) {
59
59
  export function installPmcTools({ sourceRoot, targetRoot }) {
60
60
  const srcCli = resolve(sourceRoot, 'cli');
61
61
  const srcSrc = resolve(sourceRoot, 'src');
62
+ const srcMcp = resolve(sourceRoot, 'mcp');
63
+ const srcPlugin = resolve(sourceRoot, 'plugin');
62
64
  const srcTemplates = resolve(sourceRoot, 'templates');
65
+ const srcPackageJson = resolve(sourceRoot, 'package.json');
63
66
 
64
67
  const dstBase = resolve(targetRoot, 'tools', 'project-memory-context');
65
68
  const dstCli = resolve(dstBase, 'cli');
66
69
  const dstSrc = resolve(dstBase, 'src');
70
+ const dstMcp = resolve(dstBase, 'mcp');
71
+ const dstPlugin = resolve(dstBase, 'plugin');
67
72
  const dstTemplates = resolve(dstBase, 'templates');
68
73
 
69
74
  mkdirSync(dstBase, { recursive: true });
@@ -85,15 +90,30 @@ export function installPmcTools({ sourceRoot, targetRoot }) {
85
90
  srcFiles = copyMjsTree(srcSrc, dstSrc);
86
91
  }
87
92
 
93
+ if (existsSync(srcMcp)) {
94
+ copyMjsTree(srcMcp, dstMcp);
95
+ }
96
+
97
+ if (existsSync(srcPlugin)) {
98
+ copyMjsTree(srcPlugin, dstPlugin);
99
+ }
100
+
101
+ if (existsSync(srcPackageJson)) {
102
+ copyFileSync(srcPackageJson, resolve(dstBase, 'package.json'));
103
+ }
104
+
88
105
  templateFiles = copyTemplatesDir(srcTemplates, dstTemplates);
89
106
 
90
107
  const planningBase = resolve(targetRoot, '.planning', 'project-memory-context');
108
+ const memoryDbPath = resolve(planningBase, 'memory-db');
91
109
  for (const sub of ['intake', 'graph', 'enrichment', 'memory-db', 'db']) {
92
110
  mkdirSync(resolve(planningBase, sub), { recursive: true });
93
111
  }
94
112
 
95
113
  const installState = {
96
114
  installedAt: new Date().toISOString(),
115
+ memoryDbPath,
116
+ projectRoot: resolve(targetRoot),
97
117
  sourceRoot: resolve(sourceRoot),
98
118
  version: '0.1.0',
99
119
  };
package/cli/query.mjs ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import { access } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { createQueryOrchestrator } from '../src/query/orchestrator.mjs';
7
+
8
+ function printHelp() {
9
+ console.log('Usage: pmc query <question> [--format text|json]');
10
+ console.log('');
11
+ console.log('Queries PMC project-context and symbol artifacts from the current project.');
12
+ console.log('Use --format json for machine-readable output.');
13
+ }
14
+
15
+ async function fileExists(filePath) {
16
+ try {
17
+ await access(filePath);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function findProjectRoot(startDir = process.cwd()) {
25
+ let currentDir = resolve(startDir);
26
+
27
+ while (true) {
28
+ const installPath = join(currentDir, '.planning', 'project-memory-context', 'install.json');
29
+ if (await fileExists(installPath)) {
30
+ return currentDir;
31
+ }
32
+
33
+ const parentDir = dirname(currentDir);
34
+ if (parentDir === currentDir) {
35
+ return null;
36
+ }
37
+
38
+ currentDir = parentDir;
39
+ }
40
+ }
41
+
42
+ function parseArgs(args) {
43
+ let format = 'text';
44
+ const questionParts = [];
45
+
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const arg = args[index];
48
+ if (arg === '--format') {
49
+ const value = args[index + 1];
50
+ if (!value || value.startsWith('-')) {
51
+ throw new Error('Missing value for --format. Expected text or json.');
52
+ }
53
+
54
+ format = value;
55
+ index += 1;
56
+ continue;
57
+ }
58
+
59
+ if (arg.startsWith('--')) {
60
+ throw new Error(`Unknown flag: ${arg}`);
61
+ }
62
+
63
+ questionParts.push(arg);
64
+ }
65
+
66
+ return {
67
+ format,
68
+ question: questionParts.join(' ').trim(),
69
+ };
70
+ }
71
+
72
+ function formatSource(source) {
73
+ if (source.type === 'project-context') {
74
+ return `- [project-context] ${source.title} (${source.path})`;
75
+ }
76
+
77
+ return `- [symbol] ${source.symbolKey} (${source.filePath})`;
78
+ }
79
+
80
+ function printTextResult(result) {
81
+ console.log(result.answer);
82
+ console.log('');
83
+ console.log('Sources:');
84
+ if (result.sources.length === 0) {
85
+ console.log('- none');
86
+ } else {
87
+ for (const source of result.sources) {
88
+ console.log(formatSource(source));
89
+ }
90
+ }
91
+ console.log(`tokens_saved: ${result.tokens_saved}`);
92
+ }
93
+
94
+ export async function main(args = process.argv.slice(2)) {
95
+ if (args.includes('--help') || args.includes('-h')) {
96
+ printHelp();
97
+ return 0;
98
+ }
99
+
100
+ const { format, question } = parseArgs(args);
101
+ if (format !== 'text' && format !== 'json') {
102
+ throw new Error(`Unsupported format: ${format}`);
103
+ }
104
+
105
+ if (!question) {
106
+ printHelp();
107
+ return 1;
108
+ }
109
+
110
+ const projectRoot = await findProjectRoot(process.cwd());
111
+ if (!projectRoot) {
112
+ throw new Error('pmc query must be run inside a PMC-enabled project (missing .planning/project-memory-context/install.json).');
113
+ }
114
+
115
+ const orchestrator = createQueryOrchestrator({ projectRoot });
116
+ const result = await orchestrator.query(question);
117
+
118
+ if (format === 'json') {
119
+ console.log(JSON.stringify(result, null, 2));
120
+ return 0;
121
+ }
122
+
123
+ printTextResult(result);
124
+ return 0;
125
+ }
126
+
127
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
128
+ const exitCode = await main().catch((error) => {
129
+ console.error('[query] FATAL:', error.message);
130
+ return 1;
131
+ });
132
+
133
+ if (exitCode !== 0) {
134
+ process.exit(exitCode);
135
+ }
136
+ }
package/cli/status.mjs CHANGED
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
5
5
 
6
6
  import { detectAgentType, resolveConfigDirs } from '../src/platform.mjs';
7
7
 
8
+ const DEFAULT_STALE_AFTER_SECONDS = 90;
9
+
8
10
  export function summarizeWorklist(worklist) {
9
11
  return {
10
12
  pending: worklist.filter((e) => e.status === 'pending' || e.status === 'stale').length,
@@ -21,6 +23,52 @@ async function readJsonSafe(filePath) {
21
23
  }
22
24
  }
23
25
 
26
+ function toIsoString(value) {
27
+ if (!value) return null;
28
+ const date = new Date(value);
29
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
30
+ }
31
+
32
+ function heartbeatIsFresh(heartbeatAt, now, staleAfterSeconds = DEFAULT_STALE_AFTER_SECONDS) {
33
+ if (!heartbeatAt) return false;
34
+ const heartbeat = new Date(heartbeatAt).getTime();
35
+ const current = new Date(now).getTime();
36
+ if (!Number.isFinite(heartbeat) || !Number.isFinite(current)) return false;
37
+ return current - heartbeat <= staleAfterSeconds * 1000;
38
+ }
39
+
40
+ function deriveRuntimeState(queueState, now, staleAfterSeconds = DEFAULT_STALE_AFTER_SECONDS) {
41
+ if (!queueState || typeof queueState !== 'object') {
42
+ return { state: 'idle', runtime: null };
43
+ }
44
+
45
+ const runtime = {
46
+ pid: Number.isInteger(queueState.pid) ? queueState.pid : null,
47
+ startedAt: toIsoString(queueState.startedAt),
48
+ heartbeatAt: toIsoString(queueState.heartbeatAt),
49
+ finishedAt: toIsoString(queueState.finishedAt),
50
+ staleAfterSeconds,
51
+ lastError: queueState.lastError ?? null,
52
+ };
53
+
54
+ if (queueState.status === 'finished') {
55
+ return { state: 'finished', runtime };
56
+ }
57
+
58
+ if (queueState.status === 'failed') {
59
+ return { state: 'failed', runtime };
60
+ }
61
+
62
+ if (queueState.status === 'running') {
63
+ return {
64
+ state: heartbeatIsFresh(runtime.heartbeatAt, now, staleAfterSeconds) ? 'running' : 'stalled',
65
+ runtime,
66
+ };
67
+ }
68
+
69
+ return { state: 'idle', runtime: null };
70
+ }
71
+
24
72
  async function getLastSyncTimestamp(enrichmentDir) {
25
73
  const syncManifest = join(enrichmentDir, 'sync-manifest.json');
26
74
  try {
@@ -31,16 +79,20 @@ async function getLastSyncTimestamp(enrichmentDir) {
31
79
  }
32
80
  }
33
81
 
34
- export async function buildStatusReport({ projectRoot = process.cwd() } = {}) {
82
+ export async function buildStatusReport({ projectRoot = process.cwd(), now = new Date().toISOString() } = {}) {
35
83
  const dirs = resolveConfigDirs(projectRoot);
36
84
  const planningDir = join(projectRoot, '.planning', 'project-memory-context');
37
85
  const enrichmentDir = join(planningDir, 'enrichment');
38
86
  const worklistPath = join(enrichmentDir, 'worklist.json');
39
87
  const installStatePath = join(planningDir, 'install.json');
88
+ const queueStatePath = join(enrichmentDir, 'queue-state.json');
40
89
 
41
90
  const worklist = await readJsonSafe(worklistPath);
42
91
  const installState = await readJsonSafe(installStatePath);
92
+ const queueState = await readJsonSafe(queueStatePath);
43
93
  const lastSync = await getLastSyncTimestamp(enrichmentDir);
94
+ const { state, runtime } = deriveRuntimeState(queueState, now);
95
+ const worklistSummary = Array.isArray(worklist) ? summarizeWorklist(worklist) : null;
44
96
 
45
97
  return {
46
98
  ok: true,
@@ -49,7 +101,9 @@ export async function buildStatusReport({ projectRoot = process.cwd() } = {}) {
49
101
  configLocation: dirs.projectConfig,
50
102
  agentType: detectAgentType(projectRoot),
51
103
  installState: installState ? { installedAt: installState.installedAt, version: installState.version } : null,
52
- worklist: worklist ? summarizeWorklist(worklist) : null,
104
+ state,
105
+ runtime,
106
+ worklist: worklistSummary,
53
107
  lastSync,
54
108
  };
55
109
  }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+
6
+ import { createQueryOrchestrator } from '../src/query/orchestrator.mjs';
7
+
8
+ const projectRoot = process.env.PMC_PROJECT_ROOT || process.cwd();
9
+ const orchestrator = createQueryOrchestrator({ projectRoot });
10
+
11
+ function textResult(value) {
12
+ return {
13
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
14
+ };
15
+ }
16
+
17
+ function errorResult(toolName, error) {
18
+ return {
19
+ content: [{ type: 'text', text: `${toolName} failed: ${String(error)}` }],
20
+ isError: true,
21
+ };
22
+ }
23
+
24
+ const server = new McpServer({
25
+ name: 'pmc-query',
26
+ version: '0.1.5',
27
+ });
28
+
29
+ server.tool(
30
+ 'pmc_query_project',
31
+ 'Query PMC project context and symbol artifacts using a natural-language question.',
32
+ {
33
+ question: z.string().describe('Natural-language project question'),
34
+ },
35
+ async ({ question }) => {
36
+ try {
37
+ return textResult(await orchestrator.query(question));
38
+ } catch (error) {
39
+ return errorResult('pmc_query_project', error);
40
+ }
41
+ },
42
+ );
43
+
44
+ server.tool(
45
+ 'pmc_search_symbols',
46
+ 'Search PMC symbols by semantic summary and optional file path filter.',
47
+ {
48
+ query: z.string().describe('Search query for symbols'),
49
+ file: z.string().optional().describe('Optional normalized file path filter'),
50
+ },
51
+ async ({ query, file }) => {
52
+ try {
53
+ return textResult(await orchestrator.searchSymbols(query, file));
54
+ } catch (error) {
55
+ return errorResult('pmc_search_symbols', error);
56
+ }
57
+ },
58
+ );
59
+
60
+ server.tool(
61
+ 'pmc_get_dependents',
62
+ 'List symbols that depend on the given symbol key.',
63
+ {
64
+ symbol: z.string().describe('Normalized PMC symbol key'),
65
+ },
66
+ async ({ symbol }) => {
67
+ try {
68
+ return textResult(await orchestrator.getDependents(symbol));
69
+ } catch (error) {
70
+ return errorResult('pmc_get_dependents', error);
71
+ }
72
+ },
73
+ );
74
+
75
+ server.tool(
76
+ 'pmc_get_dependencies',
77
+ 'List symbols the given symbol key depends on.',
78
+ {
79
+ symbol: z.string().describe('Normalized PMC symbol key'),
80
+ },
81
+ async ({ symbol }) => {
82
+ try {
83
+ return textResult(await orchestrator.getDependencies(symbol));
84
+ } catch (error) {
85
+ return errorResult('pmc_get_dependencies', error);
86
+ }
87
+ },
88
+ );
89
+
90
+ await server.connect(new StdioServerTransport());