@awareness-sdk/local 0.1.13 → 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/bin/awareness-local.mjs +16 -0
- package/package.json +1 -1
- package/src/mcp-stdio.mjs +398 -0
package/bin/awareness-local.mjs
CHANGED
|
@@ -433,6 +433,17 @@ async function cmdReindex(flags) {
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Run as a stdio MCP server (for IDE integrations like Claude Code).
|
|
438
|
+
*/
|
|
439
|
+
async function cmdMcp(flags) {
|
|
440
|
+
const projectDir = resolveProjectDir(flags);
|
|
441
|
+
const port = resolvePort(flags);
|
|
442
|
+
|
|
443
|
+
const { startStdioMcp } = await import('../src/mcp-stdio.mjs');
|
|
444
|
+
await startStdioMcp({ port, projectDir });
|
|
445
|
+
}
|
|
446
|
+
|
|
436
447
|
// ---------------------------------------------------------------------------
|
|
437
448
|
// Help
|
|
438
449
|
// ---------------------------------------------------------------------------
|
|
@@ -449,6 +460,7 @@ Commands:
|
|
|
449
460
|
stop Stop the daemon
|
|
450
461
|
status Show daemon status and stats
|
|
451
462
|
reindex Rebuild the search index
|
|
463
|
+
mcp Run as stdio MCP server
|
|
452
464
|
|
|
453
465
|
Options:
|
|
454
466
|
--project <dir> Project directory (default: current directory)
|
|
@@ -461,6 +473,7 @@ Examples:
|
|
|
461
473
|
npx @awareness-sdk/local status
|
|
462
474
|
npx @awareness-sdk/local stop
|
|
463
475
|
npx @awareness-sdk/local reindex --project /path/to/project
|
|
476
|
+
npx @awareness-sdk/local mcp --project /path/to/project --port 37800
|
|
464
477
|
`);
|
|
465
478
|
}
|
|
466
479
|
|
|
@@ -489,6 +502,9 @@ async function main() {
|
|
|
489
502
|
case 'reindex':
|
|
490
503
|
await cmdReindex(flags);
|
|
491
504
|
break;
|
|
505
|
+
case 'mcp':
|
|
506
|
+
await cmdMcp(flags);
|
|
507
|
+
break;
|
|
492
508
|
default:
|
|
493
509
|
console.error(`Unknown command: ${command}`);
|
|
494
510
|
printHelp();
|
package/package.json
CHANGED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-stdio.mjs — Lightweight stdio MCP proxy for Awareness Local.
|
|
3
|
+
*
|
|
4
|
+
* Registers the same 5 tools as the HTTP daemon (awareness_init,
|
|
5
|
+
* awareness_recall, awareness_record, awareness_lookup,
|
|
6
|
+
* awareness_get_agent_prompt) but proxies every call to the local daemon
|
|
7
|
+
* via HTTP JSON-RPC at http://localhost:{port}/mcp.
|
|
8
|
+
*
|
|
9
|
+
* If the daemon is not running it is auto-started before the first call.
|
|
10
|
+
*
|
|
11
|
+
* stdout is reserved for the stdio MCP protocol — all logging goes to stderr.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import http from 'node:http';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Logging — always to stderr so stdout stays clean for stdio protocol
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function log(...args) {
|
|
30
|
+
process.stderr.write(`[awareness-stdio] ${args.join(' ')}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// HTTP helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple HTTP POST that returns parsed JSON.
|
|
39
|
+
* Uses only node:http to avoid external dependencies.
|
|
40
|
+
*/
|
|
41
|
+
function httpPost(url, body) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const u = new URL(url);
|
|
44
|
+
const data = JSON.stringify(body);
|
|
45
|
+
const req = http.request(
|
|
46
|
+
{
|
|
47
|
+
hostname: u.hostname,
|
|
48
|
+
port: u.port,
|
|
49
|
+
path: u.pathname,
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Content-Length': Buffer.byteLength(data),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
(res) => {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
res.on('data', (c) => chunks.push(c));
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
try {
|
|
61
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
reject(new Error(`Failed to parse daemon response: ${e.message}`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.write(data);
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Quick health check — resolves true if daemon responds, false otherwise.
|
|
76
|
+
*/
|
|
77
|
+
function checkHealth(port) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const req = http.get(`http://127.0.0.1:${port}/healthz`, (res) => {
|
|
80
|
+
// Any response means daemon is up
|
|
81
|
+
res.resume();
|
|
82
|
+
resolve(true);
|
|
83
|
+
});
|
|
84
|
+
req.on('error', () => resolve(false));
|
|
85
|
+
req.setTimeout(2000, () => {
|
|
86
|
+
req.destroy();
|
|
87
|
+
resolve(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Daemon lifecycle
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ensure the daemon is running. If not, spawn it and poll /healthz for up
|
|
98
|
+
* to 15 seconds.
|
|
99
|
+
*/
|
|
100
|
+
async function ensureDaemon(port) {
|
|
101
|
+
if (await checkHealth(port)) return;
|
|
102
|
+
|
|
103
|
+
log('Daemon not reachable — starting...');
|
|
104
|
+
const binPath = join(__dirname, '..', 'bin', 'awareness-local.mjs');
|
|
105
|
+
const child = spawn(process.execPath, [binPath, 'start'], {
|
|
106
|
+
stdio: 'ignore',
|
|
107
|
+
detached: true,
|
|
108
|
+
env: { ...process.env, PORT: String(port) },
|
|
109
|
+
});
|
|
110
|
+
child.unref();
|
|
111
|
+
|
|
112
|
+
// Poll healthz for up to 15 seconds
|
|
113
|
+
const deadline = Date.now() + 15_000;
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
116
|
+
if (await checkHealth(port)) {
|
|
117
|
+
log('Daemon is ready.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Daemon did not become healthy within 15s (port ${port}). ` +
|
|
123
|
+
`Try running "npx awareness-local start" manually.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// JSON-RPC proxy
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
let _daemonChecked = false;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Proxy a tool call to the daemon via JSON-RPC over HTTP.
|
|
135
|
+
*
|
|
136
|
+
* @param {number} port
|
|
137
|
+
* @param {string} toolName — MCP tool name (e.g. "awareness_init")
|
|
138
|
+
* @param {object} args — tool arguments
|
|
139
|
+
* @returns {object} parsed result from daemon
|
|
140
|
+
*/
|
|
141
|
+
async function proxyCall(port, toolName, args) {
|
|
142
|
+
// Lazy daemon startup — only check once per process
|
|
143
|
+
if (!_daemonChecked) {
|
|
144
|
+
await ensureDaemon(port);
|
|
145
|
+
_daemonChecked = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const rpcBody = {
|
|
149
|
+
jsonrpc: '2.0',
|
|
150
|
+
id: 1,
|
|
151
|
+
method: 'tools/call',
|
|
152
|
+
params: { name: toolName, arguments: args },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await httpPost(`http://127.0.0.1:${port}/mcp`, rpcBody);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// Daemon may have died — try to restart once
|
|
160
|
+
log(`Proxy error, retrying after daemon restart: ${err.message}`);
|
|
161
|
+
_daemonChecked = false;
|
|
162
|
+
await ensureDaemon(port);
|
|
163
|
+
_daemonChecked = true;
|
|
164
|
+
response = await httpPost(`http://127.0.0.1:${port}/mcp`, rpcBody);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// JSON-RPC error
|
|
168
|
+
if (response.error) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Daemon RPC error ${response.error.code}: ${response.error.message}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The daemon wraps results as:
|
|
175
|
+
// result.content[0].text = JSON.stringify(payload)
|
|
176
|
+
const result = response.result;
|
|
177
|
+
if (result?.content?.[0]?.text) {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(result.content[0].text);
|
|
180
|
+
} catch {
|
|
181
|
+
// If it's not parseable JSON, return as-is
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Tool registration helpers
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wrap a proxy result in the standard MCP content envelope.
|
|
194
|
+
*/
|
|
195
|
+
function mcpResult(data) {
|
|
196
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function mcpError(message) {
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Register tools — schemas match mcp-server.mjs exactly
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function registerTools(server, port) {
|
|
211
|
+
// ======================== awareness_init ==================================
|
|
212
|
+
|
|
213
|
+
server.tool(
|
|
214
|
+
'awareness_init',
|
|
215
|
+
{
|
|
216
|
+
memory_id: z.string().optional().describe(
|
|
217
|
+
'Memory identifier (ignored in local mode, uses project dir)',
|
|
218
|
+
),
|
|
219
|
+
source: z.string().optional().describe('Client source identifier'),
|
|
220
|
+
days: z.number().optional().default(7).describe(
|
|
221
|
+
'Days of history to load',
|
|
222
|
+
),
|
|
223
|
+
max_cards: z.number().optional().default(5),
|
|
224
|
+
max_tasks: z.number().optional().default(5),
|
|
225
|
+
},
|
|
226
|
+
async (params) => {
|
|
227
|
+
try {
|
|
228
|
+
return mcpResult(await proxyCall(port, 'awareness_init', params));
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return mcpError(`awareness_init failed: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// ======================== awareness_recall ================================
|
|
236
|
+
|
|
237
|
+
server.tool(
|
|
238
|
+
'awareness_recall',
|
|
239
|
+
{
|
|
240
|
+
semantic_query: z.string().optional().default('').describe(
|
|
241
|
+
'Natural language search query (required for search)',
|
|
242
|
+
),
|
|
243
|
+
keyword_query: z.string().optional().default('').describe(
|
|
244
|
+
'Exact keyword match for BM25 full-text search',
|
|
245
|
+
),
|
|
246
|
+
scope: z.enum(['all', 'timeline', 'knowledge', 'insights'])
|
|
247
|
+
.optional().default('all')
|
|
248
|
+
.describe('Search scope'),
|
|
249
|
+
recall_mode: z.enum(['precise', 'session', 'structured', 'hybrid', 'auto'])
|
|
250
|
+
.optional().default('hybrid')
|
|
251
|
+
.describe('Search mode (hybrid recommended)'),
|
|
252
|
+
limit: z.number().min(1).max(30).optional().default(10)
|
|
253
|
+
.describe('Max results'),
|
|
254
|
+
detail: z.enum(['summary', 'full']).optional().default('summary')
|
|
255
|
+
.describe(
|
|
256
|
+
'summary = lightweight index (~50-100 tokens each); ' +
|
|
257
|
+
'full = complete content for specified ids',
|
|
258
|
+
),
|
|
259
|
+
ids: z.array(z.string()).optional().describe(
|
|
260
|
+
'Item IDs to expand when detail=full (from a prior detail=summary call)',
|
|
261
|
+
),
|
|
262
|
+
agent_role: z.string().optional().default('').describe('Agent role filter'),
|
|
263
|
+
},
|
|
264
|
+
async (params) => {
|
|
265
|
+
try {
|
|
266
|
+
return mcpResult(await proxyCall(port, 'awareness_recall', params));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return mcpError(`awareness_recall failed: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// ======================== awareness_record ================================
|
|
274
|
+
|
|
275
|
+
server.tool(
|
|
276
|
+
'awareness_record',
|
|
277
|
+
{
|
|
278
|
+
action: z.enum([
|
|
279
|
+
'remember', 'remember_batch', 'update_task', 'submit_insights',
|
|
280
|
+
]).describe('Record action type'),
|
|
281
|
+
content: z.string().optional().describe('Memory content (markdown)'),
|
|
282
|
+
title: z.string().optional().describe('Memory title'),
|
|
283
|
+
items: z.array(z.object({
|
|
284
|
+
content: z.string(),
|
|
285
|
+
title: z.string().optional(),
|
|
286
|
+
event_type: z.string().optional(),
|
|
287
|
+
tags: z.array(z.string()).optional(),
|
|
288
|
+
insights: z.any().optional(),
|
|
289
|
+
})).optional().describe('Batch items for remember_batch'),
|
|
290
|
+
insights: z.object({
|
|
291
|
+
knowledge_cards: z.array(z.any()).optional(),
|
|
292
|
+
action_items: z.array(z.any()).optional(),
|
|
293
|
+
risks: z.array(z.any()).optional(),
|
|
294
|
+
}).optional().describe('Pre-extracted knowledge cards, tasks, risks'),
|
|
295
|
+
session_id: z.string().optional(),
|
|
296
|
+
agent_role: z.string().optional(),
|
|
297
|
+
event_type: z.string().optional(),
|
|
298
|
+
tags: z.array(z.string()).optional(),
|
|
299
|
+
// Task update fields
|
|
300
|
+
task_id: z.string().optional(),
|
|
301
|
+
status: z.string().optional(),
|
|
302
|
+
},
|
|
303
|
+
async (params) => {
|
|
304
|
+
try {
|
|
305
|
+
return mcpResult(await proxyCall(port, 'awareness_record', params));
|
|
306
|
+
} catch (err) {
|
|
307
|
+
return mcpError(`awareness_record failed: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// ======================== awareness_lookup ================================
|
|
313
|
+
|
|
314
|
+
server.tool(
|
|
315
|
+
'awareness_lookup',
|
|
316
|
+
{
|
|
317
|
+
type: z.enum([
|
|
318
|
+
'context', 'tasks', 'knowledge', 'risks',
|
|
319
|
+
'session_history', 'timeline',
|
|
320
|
+
]).describe(
|
|
321
|
+
'Data type to look up. ' +
|
|
322
|
+
'context = full dump, tasks = open tasks, knowledge = cards, ' +
|
|
323
|
+
'risks = risk items, session_history = past sessions, timeline = events',
|
|
324
|
+
),
|
|
325
|
+
limit: z.number().optional().default(10).describe('Max items'),
|
|
326
|
+
status: z.string().optional().describe('Status filter'),
|
|
327
|
+
category: z.string().optional().describe('Category filter (knowledge cards)'),
|
|
328
|
+
priority: z.string().optional().describe('Priority filter (tasks/risks)'),
|
|
329
|
+
session_id: z.string().optional().describe('Session ID (for session_history)'),
|
|
330
|
+
agent_role: z.string().optional().describe('Agent role filter'),
|
|
331
|
+
query: z.string().optional().describe('Keyword filter'),
|
|
332
|
+
},
|
|
333
|
+
async (params) => {
|
|
334
|
+
try {
|
|
335
|
+
return mcpResult(await proxyCall(port, 'awareness_lookup', params));
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return mcpError(`awareness_lookup failed: ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// ======================== awareness_get_agent_prompt ======================
|
|
343
|
+
|
|
344
|
+
server.tool(
|
|
345
|
+
'awareness_get_agent_prompt',
|
|
346
|
+
{
|
|
347
|
+
role: z.string().optional().describe('Agent role to get prompt for'),
|
|
348
|
+
},
|
|
349
|
+
async (params) => {
|
|
350
|
+
try {
|
|
351
|
+
return mcpResult(await proxyCall(port, 'awareness_get_agent_prompt', params));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
return mcpError(`awareness_get_agent_prompt failed: ${err.message}`);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// Public API
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Start the stdio MCP proxy server.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} opts
|
|
367
|
+
* @param {number} [opts.port=37800] — daemon HTTP port to proxy to
|
|
368
|
+
* @param {string} [opts.projectDir] — project directory (unused in proxy,
|
|
369
|
+
* but accepted for API symmetry with direct-mode startup)
|
|
370
|
+
*/
|
|
371
|
+
export async function startStdioMcp({ port = 37800, projectDir } = {}) {
|
|
372
|
+
log(`Starting stdio MCP proxy (daemon port=${port})`);
|
|
373
|
+
|
|
374
|
+
const server = new McpServer({
|
|
375
|
+
name: 'awareness-local-stdio',
|
|
376
|
+
version: '1.0.0',
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
registerTools(server, port);
|
|
380
|
+
|
|
381
|
+
const transport = new StdioServerTransport();
|
|
382
|
+
await server.connect(transport);
|
|
383
|
+
|
|
384
|
+
log('stdio MCP proxy connected and ready.');
|
|
385
|
+
return server;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// CLI entry — run directly with `node src/mcp-stdio.mjs`
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
393
|
+
const port = parseInt(process.env.AWARENESS_PORT || process.env.PORT || '37800', 10);
|
|
394
|
+
startStdioMcp({ port }).catch((err) => {
|
|
395
|
+
process.stderr.write(`[awareness-stdio] Fatal: ${err.message}\n`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
});
|
|
398
|
+
}
|