@steno-ai/mcp 0.1.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.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-server.d.ts +20 -0
- package/dist/local-server.d.ts.map +1 -0
- package/dist/local-server.js +226 -0
- package/dist/local-server.js.map +1 -0
- package/dist/local.d.ts +3 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +94 -0
- package/dist/local.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +95 -0
- package/dist/server.js.map +1 -0
- package/package.json +37 -0
- package/src/index.ts +31 -0
- package/src/local-server.ts +381 -0
- package/src/local.ts +146 -0
- package/src/server.ts +153 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { createServer } from './server.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import Steno from '@steno-ai/sdk';
|
|
5
|
+
import { createServer } from './server.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const apiKey = process.env.STENO_API_KEY;
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
console.error('Error: STENO_API_KEY environment variable is required.\n\n' +
|
|
10
|
+
'Set it before running:\n' +
|
|
11
|
+
' export STENO_API_KEY=sk_steno_...\n\n' +
|
|
12
|
+
'Or pass it inline:\n' +
|
|
13
|
+
' STENO_API_KEY=sk_steno_... npx @steno-ai/mcp');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const steno = new Steno(apiKey);
|
|
17
|
+
const server = createServer(steno);
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
}
|
|
21
|
+
main().catch((err) => {
|
|
22
|
+
console.error('Fatal error:', err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,KAAK,MAAM,eAAe,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CACX,4DAA4D;YAC1D,0BAA0B;YAC1B,yCAAyC;YACzC,sBAAsB;YACtB,gDAAgD,CACnD,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local MCP server — connects directly to Supabase + engine.
|
|
3
|
+
* No API deployment needed. Just set env vars and go.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { StorageAdapter } from '../../engine/src/adapters/storage.js';
|
|
7
|
+
import type { EmbeddingAdapter } from '../../engine/src/adapters/embedding.js';
|
|
8
|
+
import type { LLMAdapter } from '../../engine/src/adapters/llm.js';
|
|
9
|
+
export interface LocalServerConfig {
|
|
10
|
+
storage: StorageAdapter;
|
|
11
|
+
embedding: EmbeddingAdapter;
|
|
12
|
+
cheapLLM: LLMAdapter;
|
|
13
|
+
tenantId: string;
|
|
14
|
+
scope: 'user' | 'agent' | 'session' | 'hive';
|
|
15
|
+
scopeId: string;
|
|
16
|
+
embeddingModel: string;
|
|
17
|
+
embeddingDim: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function createLocalServer(config: LocalServerConfig): McpServer;
|
|
20
|
+
//# sourceMappingURL=local-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-server.d.ts","sourceRoot":"","sources":["../src/local-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,UAAU,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAqQtE"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local MCP server — connects directly to Supabase + engine.
|
|
3
|
+
* No API deployment needed. Just set env vars and go.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export function createLocalServer(config) {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'steno-local',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
});
|
|
12
|
+
// Lazy import to avoid loading heavy modules at startup
|
|
13
|
+
let _search = null;
|
|
14
|
+
let _pipeline = null;
|
|
15
|
+
async function getSearch() {
|
|
16
|
+
if (!_search) {
|
|
17
|
+
const mod = await import('../../engine/src/retrieval/search.js');
|
|
18
|
+
_search = mod.search;
|
|
19
|
+
}
|
|
20
|
+
return _search;
|
|
21
|
+
}
|
|
22
|
+
async function getPipeline() {
|
|
23
|
+
if (!_pipeline) {
|
|
24
|
+
const mod = await import('../../engine/src/extraction/pipeline.js');
|
|
25
|
+
_pipeline = mod.runExtractionPipeline;
|
|
26
|
+
}
|
|
27
|
+
return _pipeline;
|
|
28
|
+
}
|
|
29
|
+
// ─── REMEMBER ───
|
|
30
|
+
server.tool('steno_remember', 'Store information in long-term memory. Use this to remember facts, preferences, decisions, or anything worth recalling later.', {
|
|
31
|
+
content: z.string().optional().describe('What to remember'),
|
|
32
|
+
text: z.string().optional().describe('What to remember (alias for content)'),
|
|
33
|
+
}, async (args) => {
|
|
34
|
+
const memoryText = args.content || args.text;
|
|
35
|
+
if (!memoryText) {
|
|
36
|
+
return { content: [{ type: 'text', text: 'Error: provide content or text' }] };
|
|
37
|
+
}
|
|
38
|
+
// FAST PATH: For short, already-clean facts (< 200 chars, single sentence),
|
|
39
|
+
// skip the LLM extraction pipeline and store directly. ~200ms vs ~5000ms.
|
|
40
|
+
const isSingleFact = memoryText.length < 200 && !memoryText.includes('\n') && memoryText.split('.').length <= 2;
|
|
41
|
+
if (isSingleFact) {
|
|
42
|
+
const factId = crypto.randomUUID();
|
|
43
|
+
const embedding = await config.embedding.embed(memoryText);
|
|
44
|
+
// Ensure "User" entity exists and link fact to it
|
|
45
|
+
let userEntityId;
|
|
46
|
+
try {
|
|
47
|
+
const existing = await config.storage.findEntityByCanonicalName(config.tenantId, 'user', 'person');
|
|
48
|
+
if (existing) {
|
|
49
|
+
userEntityId = existing.id;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
userEntityId = crypto.randomUUID();
|
|
53
|
+
await config.storage.createEntity({
|
|
54
|
+
id: userEntityId, tenantId: config.tenantId, name: 'User',
|
|
55
|
+
entityType: 'person', canonicalName: 'user', properties: {},
|
|
56
|
+
embedding: await config.embedding.embed('User'),
|
|
57
|
+
embeddingModel: config.embeddingModel, embeddingDim: config.embeddingDim,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
const linkToUser = async (fid) => {
|
|
63
|
+
if (userEntityId) {
|
|
64
|
+
try {
|
|
65
|
+
await config.storage.linkFactEntity(fid, userEntityId, 'mentioned');
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
// Quick dedup check — see if very similar fact exists
|
|
71
|
+
const matches = await config.storage.vectorSearch({
|
|
72
|
+
embedding,
|
|
73
|
+
tenantId: config.tenantId,
|
|
74
|
+
scope: config.scope,
|
|
75
|
+
scopeId: config.scopeId,
|
|
76
|
+
limit: 1,
|
|
77
|
+
minSimilarity: 0.85,
|
|
78
|
+
validOnly: true,
|
|
79
|
+
});
|
|
80
|
+
if (matches.length > 0) {
|
|
81
|
+
// Similar fact exists — create new version (Git-style append-only, never invalidate)
|
|
82
|
+
const oldFact = matches[0].fact;
|
|
83
|
+
await config.storage.createFact({
|
|
84
|
+
id: factId,
|
|
85
|
+
lineageId: oldFact.lineageId ?? crypto.randomUUID(),
|
|
86
|
+
tenantId: config.tenantId,
|
|
87
|
+
scope: config.scope,
|
|
88
|
+
scopeId: config.scopeId,
|
|
89
|
+
content: memoryText,
|
|
90
|
+
embeddingModel: config.embeddingModel,
|
|
91
|
+
embeddingDim: config.embeddingDim,
|
|
92
|
+
embedding,
|
|
93
|
+
importance: 0.7,
|
|
94
|
+
confidence: 1.0,
|
|
95
|
+
operation: 'update',
|
|
96
|
+
sourceType: 'api',
|
|
97
|
+
modality: 'text',
|
|
98
|
+
tags: ['direct'],
|
|
99
|
+
metadata: {},
|
|
100
|
+
contradictionStatus: 'none',
|
|
101
|
+
});
|
|
102
|
+
await linkToUser(factId);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: 'text', text: `Updated memory (new version created)` }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// No similar fact — create new
|
|
108
|
+
await config.storage.createFact({
|
|
109
|
+
id: factId,
|
|
110
|
+
lineageId: crypto.randomUUID(),
|
|
111
|
+
tenantId: config.tenantId,
|
|
112
|
+
scope: config.scope,
|
|
113
|
+
scopeId: config.scopeId,
|
|
114
|
+
content: memoryText,
|
|
115
|
+
embeddingModel: config.embeddingModel,
|
|
116
|
+
embeddingDim: config.embeddingDim,
|
|
117
|
+
embedding,
|
|
118
|
+
importance: 0.7,
|
|
119
|
+
confidence: 1.0,
|
|
120
|
+
operation: 'create',
|
|
121
|
+
sourceType: 'api',
|
|
122
|
+
modality: 'text',
|
|
123
|
+
tags: ['direct'],
|
|
124
|
+
metadata: {},
|
|
125
|
+
contradictionStatus: 'none',
|
|
126
|
+
});
|
|
127
|
+
await linkToUser(factId);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: `Remembered` }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// FULL PATH: For longer text, multi-sentence content, conversations.
|
|
133
|
+
// Runs LLM extraction + graph building + dedup. ~3-8 seconds.
|
|
134
|
+
const runPipeline = await getPipeline();
|
|
135
|
+
const result = await runPipeline({
|
|
136
|
+
storage: config.storage,
|
|
137
|
+
embedding: config.embedding,
|
|
138
|
+
cheapLLM: config.cheapLLM,
|
|
139
|
+
embeddingModel: config.embeddingModel,
|
|
140
|
+
embeddingDim: config.embeddingDim,
|
|
141
|
+
extractionTier: 'auto',
|
|
142
|
+
}, {
|
|
143
|
+
tenantId: config.tenantId,
|
|
144
|
+
scope: config.scope,
|
|
145
|
+
scopeId: config.scopeId,
|
|
146
|
+
inputType: 'raw_text',
|
|
147
|
+
data: memoryText,
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: `Remembered (${result.factsCreated} facts, ${result.entitiesCreated} entities, ${result.edgesCreated} edges)`,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
// ─── RECALL ───
|
|
159
|
+
server.tool('steno_recall', 'Search long-term memory for relevant information. Use this when you need context about the user, their preferences, past decisions, or any previously stored knowledge.', {
|
|
160
|
+
query: z.string().describe('What to search for in memory'),
|
|
161
|
+
limit: z.number().optional().describe('Max results (default 10)'),
|
|
162
|
+
}, async ({ query, limit }) => {
|
|
163
|
+
const searchFn = await getSearch();
|
|
164
|
+
const results = await searchFn({ storage: config.storage, embedding: config.embedding }, {
|
|
165
|
+
query,
|
|
166
|
+
tenantId: config.tenantId,
|
|
167
|
+
scope: config.scope,
|
|
168
|
+
scopeId: config.scopeId,
|
|
169
|
+
limit: limit ?? 10,
|
|
170
|
+
});
|
|
171
|
+
if (results.results.length === 0) {
|
|
172
|
+
return { content: [{ type: 'text', text: 'No memories found.' }] };
|
|
173
|
+
}
|
|
174
|
+
const text = results.results
|
|
175
|
+
.map((r, i) => {
|
|
176
|
+
const signals = Object.entries(r.signals)
|
|
177
|
+
.filter(([, v]) => v > 0)
|
|
178
|
+
.map(([k, v]) => `${k.replace('Score', '')}=${v.toFixed(2)}`)
|
|
179
|
+
.join(', ');
|
|
180
|
+
return `${i + 1}. [${r.score.toFixed(2)}] ${r.fact.content}${signals ? ` (${signals})` : ''}`;
|
|
181
|
+
})
|
|
182
|
+
.join('\n');
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: `Found ${results.results.length} memories (${results.durationMs}ms):\n\n${text}`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
// ─── FEEDBACK ───
|
|
193
|
+
server.tool('steno_feedback', 'Rate whether a recalled memory was useful. Helps improve future recall quality.', {
|
|
194
|
+
fact_id: z.string().describe('Memory/fact ID to rate'),
|
|
195
|
+
useful: z.boolean().describe('Was this memory useful?'),
|
|
196
|
+
}, async ({ fact_id, useful }) => {
|
|
197
|
+
await config.storage.createMemoryAccess({
|
|
198
|
+
tenantId: config.tenantId,
|
|
199
|
+
factId: fact_id,
|
|
200
|
+
query: '',
|
|
201
|
+
searchRank: 0,
|
|
202
|
+
feedbackType: useful ? 'explicit_positive' : 'explicit_negative',
|
|
203
|
+
responseTimeMs: 0,
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{ type: 'text', text: `Feedback recorded: ${useful ? 'positive' : 'negative'}` },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
// ─── STATS ───
|
|
212
|
+
server.tool('steno_stats', 'Get memory statistics — how many facts, entities, and edges are stored.', {}, async () => {
|
|
213
|
+
const facts = await config.storage.getFactsByScope(config.tenantId, config.scope, config.scopeId, { limit: 1 });
|
|
214
|
+
const entities = await config.storage.getEntitiesForTenant(config.tenantId);
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Memory stats:\n Facts: ${facts.hasMore ? '100+' : facts.data.length}\n Entities: ${entities.length}`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
return server;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=local-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-server.js","sourceRoot":"","sources":["../src/local-server.ts"],"names":[],"mappings":"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"}
|
package/dist/local.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":""}
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local Steno MCP server for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Connects directly to Supabase — no API deployment needed.
|
|
6
|
+
*
|
|
7
|
+
* Required env vars:
|
|
8
|
+
* SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
|
9
|
+
* OPENAI_API_KEY
|
|
10
|
+
* PERPLEXITY_API_KEY (optional — falls back to OpenAI embeddings)
|
|
11
|
+
*
|
|
12
|
+
* Optional:
|
|
13
|
+
* STENO_TENANT_ID (default: auto-created)
|
|
14
|
+
* STENO_SCOPE_ID (default: "default")
|
|
15
|
+
*/
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { createLocalServer } from './local-server.js';
|
|
18
|
+
async function main() {
|
|
19
|
+
// Validate required env vars
|
|
20
|
+
const supabaseUrl = process.env.SUPABASE_URL;
|
|
21
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
22
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
23
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
24
|
+
console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required.\n');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (!openaiKey) {
|
|
28
|
+
console.error('Error: OPENAI_API_KEY is required.\n');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Dynamic imports to avoid loading everything at module level
|
|
32
|
+
const { createSupabaseClient, SupabaseStorageAdapter } = await import('../../supabase-adapter/src/index.js');
|
|
33
|
+
const { OpenAILLMAdapter } = await import('../../openai-adapter/src/index.js');
|
|
34
|
+
// Set up adapters
|
|
35
|
+
const supabase = createSupabaseClient({ url: supabaseUrl, serviceRoleKey: supabaseKey });
|
|
36
|
+
const storage = new SupabaseStorageAdapter(supabase);
|
|
37
|
+
const cheapLLM = new OpenAILLMAdapter({ apiKey: openaiKey, model: 'gpt-4.1-mini' });
|
|
38
|
+
// Embedding: prefer Perplexity (cheaper, 2000 dims) else OpenAI (3072 dims)
|
|
39
|
+
let embedding;
|
|
40
|
+
let embeddingModel;
|
|
41
|
+
let embeddingDim;
|
|
42
|
+
if (process.env.PERPLEXITY_API_KEY) {
|
|
43
|
+
const { PerplexityEmbeddingAdapter } = await import('../../engine/src/adapters/perplexity-embedding.js');
|
|
44
|
+
embedding = new PerplexityEmbeddingAdapter({
|
|
45
|
+
apiKey: process.env.PERPLEXITY_API_KEY,
|
|
46
|
+
model: 'pplx-embed-v1-4b',
|
|
47
|
+
dimensions: 2000,
|
|
48
|
+
});
|
|
49
|
+
embeddingModel = 'pplx-embed-v1-4b';
|
|
50
|
+
embeddingDim = 2000;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const { OpenAIEmbeddingAdapter } = await import('../../openai-adapter/src/index.js');
|
|
54
|
+
embedding = new OpenAIEmbeddingAdapter({
|
|
55
|
+
apiKey: openaiKey,
|
|
56
|
+
model: 'text-embedding-3-large',
|
|
57
|
+
dimensions: 3072,
|
|
58
|
+
});
|
|
59
|
+
embeddingModel = 'text-embedding-3-large';
|
|
60
|
+
embeddingDim = 3072;
|
|
61
|
+
}
|
|
62
|
+
// Tenant setup
|
|
63
|
+
const tenantId = process.env.STENO_TENANT_ID || '00000000-0000-0000-0000-000000000001';
|
|
64
|
+
const scopeId = process.env.STENO_SCOPE_ID || 'default';
|
|
65
|
+
try {
|
|
66
|
+
await storage.createTenant({
|
|
67
|
+
id: tenantId,
|
|
68
|
+
name: 'Local MCP',
|
|
69
|
+
slug: `local-mcp-${Date.now()}`,
|
|
70
|
+
plan: 'enterprise',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Tenant already exists
|
|
75
|
+
}
|
|
76
|
+
// Create and start MCP server
|
|
77
|
+
const server = createLocalServer({
|
|
78
|
+
storage,
|
|
79
|
+
embedding,
|
|
80
|
+
cheapLLM,
|
|
81
|
+
tenantId,
|
|
82
|
+
scope: 'user',
|
|
83
|
+
scopeId,
|
|
84
|
+
embeddingModel,
|
|
85
|
+
embeddingDim,
|
|
86
|
+
});
|
|
87
|
+
const transport = new StdioServerTransport();
|
|
88
|
+
await server.connect(transport);
|
|
89
|
+
}
|
|
90
|
+
main().catch((err) => {
|
|
91
|
+
console.error('Fatal error:', err);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,KAAK,MAAM,eAAe,CAAC;AAwBvC,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CA+HpD"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export function createServer(steno) {
|
|
4
|
+
const server = new McpServer({
|
|
5
|
+
name: 'steno',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
});
|
|
8
|
+
server.tool('steno_remember', 'Remember information about a user or topic for future reference', {
|
|
9
|
+
user_id: z.string().describe('User identifier'),
|
|
10
|
+
content: z.string().describe('What to remember'),
|
|
11
|
+
}, async ({ user_id, content }) => {
|
|
12
|
+
const result = await steno.add(user_id, content);
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{ type: 'text', text: `Remembered. Extraction ID: ${result.extractionId}` },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
server.tool('steno_recall', 'Recall relevant memories about a user or topic', {
|
|
20
|
+
user_id: z.string().describe('User identifier'),
|
|
21
|
+
query: z.string().describe('What to recall'),
|
|
22
|
+
limit: z.number().optional().describe('Max results (default 5)'),
|
|
23
|
+
}, async ({ user_id, query, limit }) => {
|
|
24
|
+
const results = await steno.search(user_id, query, limit ?? 5);
|
|
25
|
+
if (results.results.length === 0) {
|
|
26
|
+
return { content: [{ type: 'text', text: 'No memories found.' }] };
|
|
27
|
+
}
|
|
28
|
+
const text = results.results
|
|
29
|
+
.map((r) => {
|
|
30
|
+
const dateParts = [];
|
|
31
|
+
if (r.eventDate)
|
|
32
|
+
dateParts.push(`event: ${new Date(r.eventDate).toISOString().slice(0, 10)}`);
|
|
33
|
+
if (r.documentDate)
|
|
34
|
+
dateParts.push(`doc: ${new Date(r.documentDate).toISOString().slice(0, 10)}`);
|
|
35
|
+
const dateStr = dateParts.length > 0 ? `, ${dateParts.join(', ')}` : '';
|
|
36
|
+
let line = `[Memory] ${r.content} (score: ${r.score.toFixed(2)}${dateStr})`;
|
|
37
|
+
if (r.sourceChunk) {
|
|
38
|
+
line += `\n[Source Context] ${r.sourceChunk}`;
|
|
39
|
+
}
|
|
40
|
+
line += '\n---';
|
|
41
|
+
return line;
|
|
42
|
+
})
|
|
43
|
+
.join('\n');
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text }],
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
server.tool('steno_feedback', 'Rate whether a recalled memory was useful', {
|
|
49
|
+
fact_id: z.string().describe('Memory ID to rate'),
|
|
50
|
+
useful: z.boolean().describe('Was this memory useful?'),
|
|
51
|
+
}, async ({ fact_id, useful }) => {
|
|
52
|
+
await steno.feedback(fact_id, useful);
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{ type: 'text', text: `Feedback recorded: ${useful ? 'positive' : 'negative'}` },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
// TODO: Replace raw HTTP calls with steno.profile() / steno.graph.getRelated()
|
|
60
|
+
// once the SDK is updated with profile and graph support.
|
|
61
|
+
server.tool('steno_profile', 'Get a structured profile of everything known about a user', {
|
|
62
|
+
user_id: z.string().describe('User identifier'),
|
|
63
|
+
}, async ({ user_id }) => {
|
|
64
|
+
// TODO: Replace with `await steno.profile(user_id)` once SDK supports it
|
|
65
|
+
const profile = await steno.memory.http.request('GET', `/v1/profile/${encodeURIComponent(user_id)}`);
|
|
66
|
+
let text = `Profile for ${user_id}:\n\nStatic facts:\n`;
|
|
67
|
+
text +=
|
|
68
|
+
(profile.static || []).map((f) => ` - [${f.category}] ${f.content}`).join('\n') ||
|
|
69
|
+
' (none)';
|
|
70
|
+
text += '\n\nDynamic facts:\n';
|
|
71
|
+
text += (profile.dynamic || []).map((f) => ` - ${f.content}`).join('\n') || ' (none)';
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: 'text', text }],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
server.tool('steno_graph', 'Explore entity relationships in the knowledge graph', {
|
|
77
|
+
entity_id: z.string().describe('Entity ID to explore'),
|
|
78
|
+
depth: z.number().optional().describe('Graph traversal depth (default 2)'),
|
|
79
|
+
}, async ({ entity_id, depth }) => {
|
|
80
|
+
// TODO: Replace with `await steno.graph.getRelated(entity_id, depth ?? 2)` once SDK supports it
|
|
81
|
+
const result = await steno.memory.http.request('GET', `/v1/graph/${encodeURIComponent(entity_id)}?depth=${depth ?? 2}`);
|
|
82
|
+
const entities = result.entities || [];
|
|
83
|
+
const edges = result.edges || [];
|
|
84
|
+
let text = `Graph for entity ${entity_id}:\n`;
|
|
85
|
+
text += `Entities: ${entities.length}\n`;
|
|
86
|
+
text += entities.map((e) => ` - ${e.name} (${e.entityType})`).join('\n');
|
|
87
|
+
text += `\nRelationships: ${edges.length}\n`;
|
|
88
|
+
text += edges.map((e) => ` - ${e.relation}`).join('\n');
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text }],
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
return server;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@steno-ai/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Claude Code, Claude Desktop, and other MCP clients",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/SankrityaT/steno-ai.git",
|
|
9
|
+
"directory": "packages/mcp-server"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": { "steno-mcp": "./dist/index.js" },
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"development": "./src/index.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": ["dist", "src", "README.md"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1",
|
|
29
|
+
"@steno-ai/sdk": "workspace:*",
|
|
30
|
+
"zod": "^3.25"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22",
|
|
34
|
+
"typescript": "^5.7",
|
|
35
|
+
"vitest": "^3"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
export { createServer } from './server.js';
|
|
4
|
+
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import Steno from '@steno-ai/sdk';
|
|
7
|
+
import { createServer } from './server.js';
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
const apiKey = process.env.STENO_API_KEY;
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
console.error(
|
|
13
|
+
'Error: STENO_API_KEY environment variable is required.\n\n' +
|
|
14
|
+
'Set it before running:\n' +
|
|
15
|
+
' export STENO_API_KEY=sk_steno_...\n\n' +
|
|
16
|
+
'Or pass it inline:\n' +
|
|
17
|
+
' STENO_API_KEY=sk_steno_... npx @steno-ai/mcp',
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const steno = new Steno(apiKey);
|
|
23
|
+
const server = createServer(steno);
|
|
24
|
+
const transport = new StdioServerTransport();
|
|
25
|
+
await server.connect(transport);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main().catch((err) => {
|
|
29
|
+
console.error('Fatal error:', err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local MCP server — connects directly to Supabase + engine.
|
|
3
|
+
* No API deployment needed. Just set env vars and go.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { StorageAdapter } from '../../engine/src/adapters/storage.js';
|
|
8
|
+
import type { EmbeddingAdapter } from '../../engine/src/adapters/embedding.js';
|
|
9
|
+
import type { LLMAdapter } from '../../engine/src/adapters/llm.js';
|
|
10
|
+
|
|
11
|
+
export interface LocalServerConfig {
|
|
12
|
+
storage: StorageAdapter;
|
|
13
|
+
embedding: EmbeddingAdapter;
|
|
14
|
+
cheapLLM: LLMAdapter;
|
|
15
|
+
tenantId: string;
|
|
16
|
+
scope: 'user' | 'agent' | 'session' | 'hive';
|
|
17
|
+
scopeId: string;
|
|
18
|
+
embeddingModel: string;
|
|
19
|
+
embeddingDim: number;
|
|
20
|
+
domainEntityTypes?: import('../../engine/src/config.js').DomainEntityType[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Session buffer types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
interface SessionBuffer {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
messages: string[];
|
|
29
|
+
lastActivity: Date;
|
|
30
|
+
flushTimer: ReturnType<typeof setTimeout> | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const FLUSH_DELAY_MS = 30_000; // 30 seconds of inactivity triggers flush
|
|
34
|
+
const MAX_BUFFER_SIZE = 5; // flush after 5 messages
|
|
35
|
+
|
|
36
|
+
// In-memory embedding cache — survives across tool calls within the same MCP session
|
|
37
|
+
let _embeddingCache: Map<string, { embedding: number[]; ts: number }> | null = null;
|
|
38
|
+
function getEmbeddingCache() {
|
|
39
|
+
if (!_embeddingCache) _embeddingCache = new Map();
|
|
40
|
+
return _embeddingCache;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Simple cache adapter that wraps a Map for embedding caching */
|
|
44
|
+
const embeddingCacheAdapter = {
|
|
45
|
+
async get<T>(key: string): Promise<T | null> {
|
|
46
|
+
const cache = getEmbeddingCache();
|
|
47
|
+
const entry = cache.get(key);
|
|
48
|
+
if (!entry) return null;
|
|
49
|
+
// TTL: 10 minutes
|
|
50
|
+
if (Date.now() - entry.ts > 600_000) {
|
|
51
|
+
cache.delete(key);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return entry.embedding as unknown as T;
|
|
55
|
+
},
|
|
56
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
57
|
+
const cache = getEmbeddingCache();
|
|
58
|
+
cache.set(key, { embedding: value as unknown as number[], ts: Date.now() });
|
|
59
|
+
// Evict old entries if cache grows too large (>500 entries)
|
|
60
|
+
if (cache.size > 500) {
|
|
61
|
+
const oldest = [...cache.entries()].sort((a, b) => a[1].ts - b[1].ts).slice(0, 100);
|
|
62
|
+
for (const [k] of oldest) cache.delete(k);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async del(key: string): Promise<void> { getEmbeddingCache().delete(key); },
|
|
66
|
+
async incr(): Promise<number> { return 0; },
|
|
67
|
+
async expire(): Promise<void> {},
|
|
68
|
+
async ping(): Promise<boolean> { return true; },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function createLocalServer(config: LocalServerConfig): McpServer {
|
|
72
|
+
const server = new McpServer({
|
|
73
|
+
name: 'steno-local',
|
|
74
|
+
version: '0.1.0',
|
|
75
|
+
instructions: `You have access to the user's persistent long-term memory via Steno.
|
|
76
|
+
|
|
77
|
+
CRITICAL RULES:
|
|
78
|
+
1. ALWAYS call steno_recall BEFORE answering ANY question about the user, their life, work, projects, people they know, preferences, past events, companies, or decisions. Do this BEFORE using web search or "Relevant chats".
|
|
79
|
+
2. When the user shares personal information, experiences, opinions, or decisions, call steno_remember to store it.
|
|
80
|
+
3. Never say "I don't have information about that" without first checking steno_recall.
|
|
81
|
+
4. Steno memory persists across ALL conversations — it knows things from past sessions that your conversation history does not.`,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Lazy import to avoid loading heavy modules at startup
|
|
85
|
+
let _search: typeof import('../../engine/src/retrieval/search.js').search | null = null;
|
|
86
|
+
let _pipeline: typeof import('../../engine/src/extraction/pipeline.js').runExtractionPipeline | null = null;
|
|
87
|
+
let _getOrCreateActiveSession: typeof import('../../engine/src/sessions/manager.js').getOrCreateActiveSession | null = null;
|
|
88
|
+
|
|
89
|
+
async function getSearch() {
|
|
90
|
+
if (!_search) {
|
|
91
|
+
const mod = await import('../../engine/src/retrieval/search.js');
|
|
92
|
+
_search = mod.search;
|
|
93
|
+
}
|
|
94
|
+
return _search;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getPipeline() {
|
|
98
|
+
if (!_pipeline) {
|
|
99
|
+
const mod = await import('../../engine/src/extraction/pipeline.js');
|
|
100
|
+
_pipeline = mod.runExtractionPipeline;
|
|
101
|
+
}
|
|
102
|
+
return _pipeline;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function getSessionManager() {
|
|
106
|
+
if (!_getOrCreateActiveSession) {
|
|
107
|
+
const mod = await import('../../engine/src/sessions/manager.js');
|
|
108
|
+
_getOrCreateActiveSession = mod.getOrCreateActiveSession;
|
|
109
|
+
}
|
|
110
|
+
return _getOrCreateActiveSession;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Session buffer — accumulate messages, flush periodically
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
const sessionBuffers = new Map<string, SessionBuffer>();
|
|
117
|
+
|
|
118
|
+
/** Build a buffer key from scope parameters */
|
|
119
|
+
function bufferKey(): string {
|
|
120
|
+
return `${config.tenantId}:${config.scope}:${config.scopeId}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Flush the session buffer: run extraction pipeline on all accumulated messages */
|
|
124
|
+
async function flushBuffer(key: string): Promise<void> {
|
|
125
|
+
const buf = sessionBuffers.get(key);
|
|
126
|
+
if (!buf || buf.messages.length === 0) return;
|
|
127
|
+
|
|
128
|
+
// Grab and clear the buffer immediately so new messages start a fresh batch
|
|
129
|
+
const messages = [...buf.messages];
|
|
130
|
+
const sessionId = buf.sessionId;
|
|
131
|
+
buf.messages = [];
|
|
132
|
+
if (buf.flushTimer) {
|
|
133
|
+
clearTimeout(buf.flushTimer);
|
|
134
|
+
buf.flushTimer = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fullText = messages.join('\n---\n');
|
|
138
|
+
|
|
139
|
+
console.error(`[steno] Flushing session buffer: ${messages.length} messages, sessionId=${sessionId}`);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const runPipeline = await getPipeline();
|
|
143
|
+
const result = await runPipeline(
|
|
144
|
+
{
|
|
145
|
+
storage: config.storage,
|
|
146
|
+
embedding: config.embedding,
|
|
147
|
+
cheapLLM: config.cheapLLM,
|
|
148
|
+
embeddingModel: config.embeddingModel,
|
|
149
|
+
embeddingDim: config.embeddingDim,
|
|
150
|
+
extractionTier: 'auto',
|
|
151
|
+
domainEntityTypes: config.domainEntityTypes,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
tenantId: config.tenantId,
|
|
155
|
+
scope: config.scope,
|
|
156
|
+
scopeId: config.scopeId,
|
|
157
|
+
sessionId,
|
|
158
|
+
inputType: 'raw_text',
|
|
159
|
+
data: fullText,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
console.error(`[steno] Session flush done: ${result.factsCreated} facts, ${result.entitiesCreated} entities, ${result.edgesCreated} edges`);
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
console.error('[steno] Session flush pipeline error:', err?.message ?? err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Schedule a flush after the inactivity delay, or flush immediately if buffer is full */
|
|
169
|
+
function scheduleFlush(key: string): void {
|
|
170
|
+
const buf = sessionBuffers.get(key);
|
|
171
|
+
if (!buf) return;
|
|
172
|
+
|
|
173
|
+
// Clear any existing timer
|
|
174
|
+
if (buf.flushTimer) {
|
|
175
|
+
clearTimeout(buf.flushTimer);
|
|
176
|
+
buf.flushTimer = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Flush immediately if buffer is full
|
|
180
|
+
if (buf.messages.length >= MAX_BUFFER_SIZE) {
|
|
181
|
+
void flushBuffer(key);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Otherwise schedule a delayed flush
|
|
186
|
+
buf.flushTimer = setTimeout(() => {
|
|
187
|
+
void flushBuffer(key);
|
|
188
|
+
}, FLUSH_DELAY_MS);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── REMEMBER ───
|
|
192
|
+
server.tool(
|
|
193
|
+
'steno_remember',
|
|
194
|
+
'Store important information in the user\'s persistent long-term memory. ALWAYS use this to save facts, preferences, decisions, experiences, people, companies, events, or anything the user shares that they might want recalled later. This memory persists across ALL conversations and devices.',
|
|
195
|
+
{
|
|
196
|
+
content: z.string().optional().describe('What to remember'),
|
|
197
|
+
text: z.string().optional().describe('What to remember (alias for content)'),
|
|
198
|
+
},
|
|
199
|
+
async (args) => {
|
|
200
|
+
const memoryText = args.content || args.text;
|
|
201
|
+
if (!memoryText) {
|
|
202
|
+
return { content: [{ type: 'text' as const, text: 'Error: provide content or text' }] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Session-based buffering ──
|
|
206
|
+
// Accumulate messages within a session. Extraction runs when the buffer
|
|
207
|
+
// is full (MAX_BUFFER_SIZE) or after an inactivity timeout (FLUSH_DELAY_MS).
|
|
208
|
+
const key = bufferKey();
|
|
209
|
+
let buf = sessionBuffers.get(key);
|
|
210
|
+
|
|
211
|
+
if (!buf) {
|
|
212
|
+
// Start or resume a session
|
|
213
|
+
let sessionId: string;
|
|
214
|
+
try {
|
|
215
|
+
const getOrCreate = await getSessionManager();
|
|
216
|
+
// SessionScope excludes 'session', but config.scope might be 'session'.
|
|
217
|
+
// Treat 'session' scope as 'user' for session tracking purposes.
|
|
218
|
+
const sessionScope = config.scope === 'session' ? 'user' : config.scope;
|
|
219
|
+
const session = await getOrCreate(config.storage, config.tenantId, sessionScope as any, config.scopeId);
|
|
220
|
+
sessionId = session.id;
|
|
221
|
+
} catch (err: any) {
|
|
222
|
+
console.error('[steno] Failed to create session, using ephemeral ID:', err?.message ?? err);
|
|
223
|
+
sessionId = crypto.randomUUID();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
buf = {
|
|
227
|
+
sessionId,
|
|
228
|
+
messages: [],
|
|
229
|
+
lastActivity: new Date(),
|
|
230
|
+
flushTimer: null,
|
|
231
|
+
};
|
|
232
|
+
sessionBuffers.set(key, buf);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Buffer the message
|
|
236
|
+
buf.messages.push(memoryText);
|
|
237
|
+
buf.lastActivity = new Date();
|
|
238
|
+
|
|
239
|
+
// ALL messages go through the full pipeline via session buffer (no fast path).
|
|
240
|
+
// Full pipeline gives us: LLM extraction, entity/edge creation,
|
|
241
|
+
// contextual embeddings, temporal grounding, dedup — everything.
|
|
242
|
+
// Returns immediately — extraction happens in the background when flushed.
|
|
243
|
+
scheduleFlush(key);
|
|
244
|
+
|
|
245
|
+
const pending = buf.messages.length;
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: 'text' as const, text: `Buffered (${pending}/${MAX_BUFFER_SIZE} messages in session). Extraction runs on flush.` }],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// ─── FLUSH ───
|
|
253
|
+
server.tool(
|
|
254
|
+
'steno_flush',
|
|
255
|
+
'Force extraction of all buffered session messages. Use before searching if you just stored information and need it immediately available.',
|
|
256
|
+
{},
|
|
257
|
+
async () => {
|
|
258
|
+
const key = bufferKey();
|
|
259
|
+
const buf = sessionBuffers.get(key);
|
|
260
|
+
if (!buf || buf.messages.length === 0) {
|
|
261
|
+
return { content: [{ type: 'text' as const, text: 'No buffered messages to flush.' }] };
|
|
262
|
+
}
|
|
263
|
+
const count = buf.messages.length;
|
|
264
|
+
await flushBuffer(key);
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: 'text' as const, text: `Flushed ${count} buffered messages. Extraction complete.` }],
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// ─── RECALL ───
|
|
272
|
+
server.tool(
|
|
273
|
+
'steno_recall',
|
|
274
|
+
'ALWAYS search this memory before answering questions about the user, their life, work, projects, preferences, people they know, companies, events, or anything personal. This contains the user\'s persistent memory across all conversations. Search here FIRST before using web search or saying you don\'t know.',
|
|
275
|
+
{
|
|
276
|
+
query: z.string().describe('What to search for in memory'),
|
|
277
|
+
limit: z.number().optional().describe('Max results (default 10)'),
|
|
278
|
+
},
|
|
279
|
+
async ({ query, limit }) => {
|
|
280
|
+
// Auto-flush any pending buffered messages before searching
|
|
281
|
+
const key = bufferKey();
|
|
282
|
+
const buf = sessionBuffers.get(key);
|
|
283
|
+
if (buf && buf.messages.length > 0) {
|
|
284
|
+
await flushBuffer(key);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const searchFn = await getSearch();
|
|
288
|
+
const results = await searchFn(
|
|
289
|
+
{ storage: config.storage, embedding: config.embedding, cache: embeddingCacheAdapter as any },
|
|
290
|
+
{
|
|
291
|
+
query,
|
|
292
|
+
tenantId: config.tenantId,
|
|
293
|
+
scope: config.scope,
|
|
294
|
+
scopeId: config.scopeId,
|
|
295
|
+
limit: limit ?? 10,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (results.results.length === 0) {
|
|
300
|
+
return { content: [{ type: 'text' as const, text: 'No memories found.' }] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const text = results.results
|
|
304
|
+
.map((r) => {
|
|
305
|
+
const dateParts: string[] = [];
|
|
306
|
+
if (r.fact.eventDate) dateParts.push(`event: ${new Date(r.fact.eventDate).toISOString().slice(0, 10)}`);
|
|
307
|
+
if (r.fact.documentDate) dateParts.push(`doc: ${new Date(r.fact.documentDate).toISOString().slice(0, 10)}`);
|
|
308
|
+
const dateStr = dateParts.length > 0 ? `, ${dateParts.join(', ')}` : '';
|
|
309
|
+
const signals = Object.entries(r.signals)
|
|
310
|
+
.filter(([, v]) => v > 0)
|
|
311
|
+
.map(([k, v]) => `${k.replace('Score', '')}=${(v as number).toFixed(2)}`)
|
|
312
|
+
.join(', ');
|
|
313
|
+
let line = `[Memory] ${r.fact.content} (score: ${r.score.toFixed(2)}${dateStr}${signals ? `, ${signals}` : ''})`;
|
|
314
|
+
if (r.fact.sourceChunk) {
|
|
315
|
+
line += `\n[Source Context] ${r.fact.sourceChunk}`;
|
|
316
|
+
}
|
|
317
|
+
line += '\n---';
|
|
318
|
+
return line;
|
|
319
|
+
})
|
|
320
|
+
.join('\n');
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: 'text' as const,
|
|
326
|
+
text: `Found ${results.results.length} memories (${results.durationMs}ms):\n\n${text}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// ─── FEEDBACK ───
|
|
334
|
+
server.tool(
|
|
335
|
+
'steno_feedback',
|
|
336
|
+
'Rate whether a recalled memory was useful. Helps improve future recall quality.',
|
|
337
|
+
{
|
|
338
|
+
fact_id: z.string().describe('Memory/fact ID to rate'),
|
|
339
|
+
useful: z.boolean().describe('Was this memory useful?'),
|
|
340
|
+
},
|
|
341
|
+
async ({ fact_id, useful }) => {
|
|
342
|
+
await config.storage.createMemoryAccess({
|
|
343
|
+
tenantId: config.tenantId,
|
|
344
|
+
factId: fact_id,
|
|
345
|
+
query: '',
|
|
346
|
+
searchRank: 0,
|
|
347
|
+
feedbackType: useful ? 'explicit_positive' : 'explicit_negative',
|
|
348
|
+
responseTimeMs: 0,
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{ type: 'text' as const, text: `Feedback recorded: ${useful ? 'positive' : 'negative'}` },
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// ─── STATS ───
|
|
359
|
+
server.tool(
|
|
360
|
+
'steno_stats',
|
|
361
|
+
'Get memory statistics — how many facts, entities, and edges are stored.',
|
|
362
|
+
{},
|
|
363
|
+
async () => {
|
|
364
|
+
const facts = await config.storage.getFactsByScope(
|
|
365
|
+
config.tenantId, config.scope, config.scopeId, { limit: 1 },
|
|
366
|
+
);
|
|
367
|
+
const entities = await config.storage.getEntitiesForTenant(config.tenantId, { limit: 1 });
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
type: 'text' as const,
|
|
373
|
+
text: `Memory stats:\n Facts: ${facts.hasMore ? '100+' : facts.data.length}\n Entities: ${entities.hasMore ? '100+' : entities.data.length}`,
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return server;
|
|
381
|
+
}
|
package/src/local.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local Steno MCP server for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Connects directly to Supabase — no API deployment needed.
|
|
6
|
+
*
|
|
7
|
+
* Required env vars:
|
|
8
|
+
* SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
|
9
|
+
* OPENAI_API_KEY
|
|
10
|
+
* PERPLEXITY_API_KEY (optional — falls back to OpenAI embeddings)
|
|
11
|
+
*
|
|
12
|
+
* Optional:
|
|
13
|
+
* STENO_TENANT_ID (default: auto-created)
|
|
14
|
+
* STENO_SCOPE_ID (default: "default")
|
|
15
|
+
*/
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { createLocalServer } from './local-server.js';
|
|
18
|
+
|
|
19
|
+
async function main(): Promise<void> {
|
|
20
|
+
// Validate required env vars
|
|
21
|
+
const supabaseUrl = process.env.SUPABASE_URL;
|
|
22
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
23
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
24
|
+
|
|
25
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
26
|
+
console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required.\n');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (!openaiKey) {
|
|
30
|
+
console.error('Error: OPENAI_API_KEY is required.\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Dynamic imports to avoid loading everything at module level
|
|
35
|
+
const { createSupabaseClient, SupabaseStorageAdapter } = await import(
|
|
36
|
+
'../../supabase-adapter/src/index.js'
|
|
37
|
+
);
|
|
38
|
+
const { OpenAILLMAdapter } = await import('../../openai-adapter/src/index.js');
|
|
39
|
+
|
|
40
|
+
// Set up adapters
|
|
41
|
+
const supabase = createSupabaseClient({ url: supabaseUrl, serviceRoleKey: supabaseKey });
|
|
42
|
+
const storage = new SupabaseStorageAdapter(supabase);
|
|
43
|
+
const cheapLLM = new OpenAILLMAdapter({ apiKey: openaiKey, model: 'gpt-5.4-mini' });
|
|
44
|
+
|
|
45
|
+
// Embedding: prefer Perplexity (cheaper, 2000 dims) else OpenAI (3072 dims)
|
|
46
|
+
let embedding: any;
|
|
47
|
+
let embeddingModel: string;
|
|
48
|
+
let embeddingDim: number;
|
|
49
|
+
|
|
50
|
+
if (process.env.PERPLEXITY_API_KEY) {
|
|
51
|
+
const { PerplexityEmbeddingAdapter } = await import(
|
|
52
|
+
'../../engine/src/adapters/perplexity-embedding.js'
|
|
53
|
+
);
|
|
54
|
+
embedding = new PerplexityEmbeddingAdapter({
|
|
55
|
+
apiKey: process.env.PERPLEXITY_API_KEY,
|
|
56
|
+
model: 'pplx-embed-v1-4b',
|
|
57
|
+
dimensions: 2000,
|
|
58
|
+
});
|
|
59
|
+
embeddingModel = 'pplx-embed-v1-4b';
|
|
60
|
+
embeddingDim = 2000;
|
|
61
|
+
} else {
|
|
62
|
+
const { OpenAIEmbeddingAdapter } = await import('../../openai-adapter/src/index.js');
|
|
63
|
+
embedding = new OpenAIEmbeddingAdapter({
|
|
64
|
+
apiKey: openaiKey,
|
|
65
|
+
model: 'text-embedding-3-large',
|
|
66
|
+
dimensions: 3072,
|
|
67
|
+
});
|
|
68
|
+
embeddingModel = 'text-embedding-3-large';
|
|
69
|
+
embeddingDim = 3072;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tenant setup
|
|
73
|
+
const tenantId = process.env.STENO_TENANT_ID || '00000000-0000-0000-0000-000000000001';
|
|
74
|
+
const scopeId = process.env.STENO_SCOPE_ID || 'default';
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await storage.createTenant({
|
|
78
|
+
id: tenantId,
|
|
79
|
+
name: 'Local MCP',
|
|
80
|
+
slug: `local-mcp-${Date.now()}`,
|
|
81
|
+
plan: 'enterprise',
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
// Tenant already exists
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create and start MCP server
|
|
88
|
+
const server = createLocalServer({
|
|
89
|
+
storage,
|
|
90
|
+
embedding,
|
|
91
|
+
cheapLLM,
|
|
92
|
+
tenantId,
|
|
93
|
+
scope: 'user',
|
|
94
|
+
scopeId,
|
|
95
|
+
embeddingModel,
|
|
96
|
+
embeddingDim,
|
|
97
|
+
domainEntityTypes: [
|
|
98
|
+
{
|
|
99
|
+
name: 'vehicle',
|
|
100
|
+
description: 'A car, truck, motorcycle, or other vehicle owned, wanted, or discussed by the user',
|
|
101
|
+
fields: [
|
|
102
|
+
{ name: 'make', type: 'string' as const, description: 'Manufacturer (Mercedes, BMW, Toyota, etc.)', required: false },
|
|
103
|
+
{ name: 'model', type: 'string' as const, description: 'Model name (E350, M3, Camry, etc.)', required: false },
|
|
104
|
+
{ name: 'year', type: 'string' as const, description: 'Model year', required: false },
|
|
105
|
+
{ name: 'ownership', type: 'string' as const, description: 'owned, wanted, previously_owned, test_driven', required: false },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'startup',
|
|
110
|
+
description: 'A startup company discussed in context of jobs, investments, partnerships, or competition',
|
|
111
|
+
fields: [
|
|
112
|
+
{ name: 'stage', type: 'string' as const, description: 'pre-seed, seed, series-a, series-b, growth, public', required: false },
|
|
113
|
+
{ name: 'relationship', type: 'string' as const, description: 'competitor, partner, prospect, employer, rejected', required: false },
|
|
114
|
+
{ name: 'funding', type: 'string' as const, description: 'Known funding amount or status', required: false },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'project',
|
|
119
|
+
description: 'A software project, product, or side project the user is building or has built',
|
|
120
|
+
fields: [
|
|
121
|
+
{ name: 'status', type: 'string' as const, description: 'active, paused, completed, abandoned', required: false },
|
|
122
|
+
{ name: 'tech_stack', type: 'string' as const, description: 'Primary technologies used', required: false },
|
|
123
|
+
{ name: 'user_role', type: 'string' as const, description: 'founder, lead, contributor, user', required: false },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Handle EPIPE gracefully — Claude Desktop may disconnect mid-response
|
|
130
|
+
process.stdout.on('error', (err: NodeJS.ErrnoException) => {
|
|
131
|
+
if (err.code === 'EPIPE') return; // Client disconnected, ignore
|
|
132
|
+
console.error('[steno] stdout error:', err);
|
|
133
|
+
});
|
|
134
|
+
process.stdin.on('error', (err: NodeJS.ErrnoException) => {
|
|
135
|
+
if (err.code === 'EPIPE') return;
|
|
136
|
+
console.error('[steno] stdin error:', err);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const transport = new StdioServerTransport();
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().catch((err) => {
|
|
144
|
+
console.error('Fatal error:', err);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type Steno from '@steno-ai/sdk';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// TODO: Remove these helper types once the SDK exports profile/graph types
|
|
6
|
+
interface ProfileFact {
|
|
7
|
+
category?: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
interface ProfileResponse {
|
|
11
|
+
static?: ProfileFact[];
|
|
12
|
+
dynamic?: ProfileFact[];
|
|
13
|
+
}
|
|
14
|
+
interface GraphEntity {
|
|
15
|
+
name: string;
|
|
16
|
+
entityType: string;
|
|
17
|
+
}
|
|
18
|
+
interface GraphEdge {
|
|
19
|
+
relation: string;
|
|
20
|
+
}
|
|
21
|
+
interface GraphResponse {
|
|
22
|
+
entities?: GraphEntity[];
|
|
23
|
+
edges?: GraphEdge[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createServer(steno: Steno): McpServer {
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: 'steno',
|
|
29
|
+
version: '0.1.0',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
server.tool(
|
|
33
|
+
'steno_remember',
|
|
34
|
+
'Remember information about a user or topic for future reference',
|
|
35
|
+
{
|
|
36
|
+
user_id: z.string().describe('User identifier'),
|
|
37
|
+
content: z.string().describe('What to remember'),
|
|
38
|
+
},
|
|
39
|
+
async ({ user_id, content }) => {
|
|
40
|
+
const result = await steno.add(user_id, content);
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{ type: 'text' as const, text: `Remembered. Extraction ID: ${result.extractionId}` },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
server.tool(
|
|
50
|
+
'steno_recall',
|
|
51
|
+
'Recall relevant memories about a user or topic',
|
|
52
|
+
{
|
|
53
|
+
user_id: z.string().describe('User identifier'),
|
|
54
|
+
query: z.string().describe('What to recall'),
|
|
55
|
+
limit: z.number().optional().describe('Max results (default 5)'),
|
|
56
|
+
},
|
|
57
|
+
async ({ user_id, query, limit }) => {
|
|
58
|
+
const results = await steno.search(user_id, query, limit ?? 5);
|
|
59
|
+
if (results.results.length === 0) {
|
|
60
|
+
return { content: [{ type: 'text' as const, text: 'No memories found.' }] };
|
|
61
|
+
}
|
|
62
|
+
const text = results.results
|
|
63
|
+
.map((r: any) => {
|
|
64
|
+
const dateParts: string[] = [];
|
|
65
|
+
if (r.eventDate) dateParts.push(`event: ${new Date(r.eventDate).toISOString().slice(0, 10)}`);
|
|
66
|
+
if (r.documentDate) dateParts.push(`doc: ${new Date(r.documentDate).toISOString().slice(0, 10)}`);
|
|
67
|
+
const dateStr = dateParts.length > 0 ? `, ${dateParts.join(', ')}` : '';
|
|
68
|
+
let line = `[Memory] ${r.content} (score: ${r.score.toFixed(2)}${dateStr})`;
|
|
69
|
+
if (r.sourceChunk) {
|
|
70
|
+
line += `\n[Source Context] ${r.sourceChunk}`;
|
|
71
|
+
}
|
|
72
|
+
line += '\n---';
|
|
73
|
+
return line;
|
|
74
|
+
})
|
|
75
|
+
.join('\n');
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: 'text' as const, text }],
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
server.tool(
|
|
83
|
+
'steno_feedback',
|
|
84
|
+
'Rate whether a recalled memory was useful',
|
|
85
|
+
{
|
|
86
|
+
fact_id: z.string().describe('Memory ID to rate'),
|
|
87
|
+
useful: z.boolean().describe('Was this memory useful?'),
|
|
88
|
+
},
|
|
89
|
+
async ({ fact_id, useful }) => {
|
|
90
|
+
await steno.feedback(fact_id, useful);
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{ type: 'text' as const, text: `Feedback recorded: ${useful ? 'positive' : 'negative'}` },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// TODO: Replace raw HTTP calls with steno.profile() / steno.graph.getRelated()
|
|
100
|
+
// once the SDK is updated with profile and graph support.
|
|
101
|
+
|
|
102
|
+
server.tool(
|
|
103
|
+
'steno_profile',
|
|
104
|
+
'Get a structured profile of everything known about a user',
|
|
105
|
+
{
|
|
106
|
+
user_id: z.string().describe('User identifier'),
|
|
107
|
+
},
|
|
108
|
+
async ({ user_id }) => {
|
|
109
|
+
// TODO: Replace with `await steno.profile(user_id)` once SDK supports it
|
|
110
|
+
const profile = await (steno as any).memory.http.request(
|
|
111
|
+
'GET',
|
|
112
|
+
`/v1/profile/${encodeURIComponent(user_id)}`,
|
|
113
|
+
) as ProfileResponse;
|
|
114
|
+
let text = `Profile for ${user_id}:\n\nStatic facts:\n`;
|
|
115
|
+
text +=
|
|
116
|
+
(profile.static || []).map((f: ProfileFact) => ` - [${f.category}] ${f.content}`).join('\n') ||
|
|
117
|
+
' (none)';
|
|
118
|
+
text += '\n\nDynamic facts:\n';
|
|
119
|
+
text += (profile.dynamic || []).map((f: ProfileFact) => ` - ${f.content}`).join('\n') || ' (none)';
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: 'text' as const, text }],
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'steno_graph',
|
|
128
|
+
'Explore entity relationships in the knowledge graph',
|
|
129
|
+
{
|
|
130
|
+
entity_id: z.string().describe('Entity ID to explore'),
|
|
131
|
+
depth: z.number().optional().describe('Graph traversal depth (default 2)'),
|
|
132
|
+
},
|
|
133
|
+
async ({ entity_id, depth }) => {
|
|
134
|
+
// TODO: Replace with `await steno.graph.getRelated(entity_id, depth ?? 2)` once SDK supports it
|
|
135
|
+
const result = await (steno as any).memory.http.request(
|
|
136
|
+
'GET',
|
|
137
|
+
`/v1/graph/${encodeURIComponent(entity_id)}?depth=${depth ?? 2}`,
|
|
138
|
+
) as GraphResponse;
|
|
139
|
+
const entities = result.entities || [];
|
|
140
|
+
const edges = result.edges || [];
|
|
141
|
+
let text = `Graph for entity ${entity_id}:\n`;
|
|
142
|
+
text += `Entities: ${entities.length}\n`;
|
|
143
|
+
text += entities.map((e: GraphEntity) => ` - ${e.name} (${e.entityType})`).join('\n');
|
|
144
|
+
text += `\nRelationships: ${edges.length}\n`;
|
|
145
|
+
text += edges.map((e: GraphEdge) => ` - ${e.relation}`).join('\n');
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text' as const, text }],
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return server;
|
|
153
|
+
}
|