smritea-mcp 0.1.1
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.
Potentially problematic release.
This version of smritea-mcp might be problematic. Click here for more details.
- package/.eslintrc.json +19 -0
- package/.pre-commit-config.yaml +30 -0
- package/Makefile +20 -0
- package/README.md +214 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +40 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +63 -0
- package/dist/tools/app.d.ts +15 -0
- package/dist/tools/app.d.ts.map +1 -0
- package/dist/tools/app.js +51 -0
- package/dist/tools/memory.d.ts +12 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +77 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/package.json +32 -0
- package/src/config.ts +67 -0
- package/src/errors.ts +15 -0
- package/src/index.ts +62 -0
- package/src/server.ts +119 -0
- package/src/tools/app.ts +59 -0
- package/src/tools/memory.ts +102 -0
- package/src/types.ts +35 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SmriteaError, SmriteaRateLimitError } from 'smritea-sdk';
|
|
2
|
+
export function formatMemory(memory) {
|
|
3
|
+
return JSON.stringify(memory, null, 2);
|
|
4
|
+
}
|
|
5
|
+
export function formatSearchResult(result) {
|
|
6
|
+
return JSON.stringify({ score: result.score, memory: result.memory }, null, 2);
|
|
7
|
+
}
|
|
8
|
+
export function formatError(err) {
|
|
9
|
+
if (err instanceof SmriteaRateLimitError && err.retryAfter !== undefined) {
|
|
10
|
+
return `${err.message} (retry after ${err.retryAfter}s)`;
|
|
11
|
+
}
|
|
12
|
+
if (err instanceof SmriteaError) {
|
|
13
|
+
return err.message;
|
|
14
|
+
}
|
|
15
|
+
return String(err);
|
|
16
|
+
}
|
|
17
|
+
export async function handleAddMemory(client, input, firstPersonUserId) {
|
|
18
|
+
try {
|
|
19
|
+
const memory = await client.add(input.content, {
|
|
20
|
+
userId: input.user_id ?? firstPersonUserId,
|
|
21
|
+
metadata: input.metadata,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: formatMemory(memory) }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function handleSearchMemories(client, input, firstPersonUserId) {
|
|
32
|
+
try {
|
|
33
|
+
const results = await client.search(input.query, {
|
|
34
|
+
userId: input.user_id ?? firstPersonUserId,
|
|
35
|
+
limit: input.limit,
|
|
36
|
+
method: input.method,
|
|
37
|
+
threshold: input.threshold,
|
|
38
|
+
graphDepth: input.graph_depth,
|
|
39
|
+
conversationId: input.conversation_id,
|
|
40
|
+
});
|
|
41
|
+
if (results.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: 'No memories found.' }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const formatted = JSON.stringify(results.map((r) => ({ score: r.score, memory: r.memory })), null, 2);
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: formatted }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function handleGetMemory(client, input) {
|
|
56
|
+
try {
|
|
57
|
+
const memory = await client.get(input.memory_id);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: formatMemory(memory) }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function handleDeleteMemory(client, input) {
|
|
67
|
+
try {
|
|
68
|
+
await client.delete(input.memory_id);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: `Memory ${input.memory_id} deleted.` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=memory.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const AddMemoryInput: z.ZodObject<{
|
|
3
|
+
content: z.ZodString;
|
|
4
|
+
user_id: z.ZodOptional<z.ZodString>;
|
|
5
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
content: string;
|
|
8
|
+
user_id?: string | undefined;
|
|
9
|
+
metadata?: Record<string, unknown> | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
content: string;
|
|
12
|
+
user_id?: string | undefined;
|
|
13
|
+
metadata?: Record<string, unknown> | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type AddMemoryInput = z.infer<typeof AddMemoryInput>;
|
|
16
|
+
export declare const SearchMemoriesInput: z.ZodObject<{
|
|
17
|
+
query: z.ZodString;
|
|
18
|
+
user_id: z.ZodOptional<z.ZodString>;
|
|
19
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
method: z.ZodOptional<z.ZodString>;
|
|
21
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
graph_depth: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
conversation_id: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
query: string;
|
|
26
|
+
user_id?: string | undefined;
|
|
27
|
+
limit?: number | undefined;
|
|
28
|
+
method?: string | undefined;
|
|
29
|
+
threshold?: number | undefined;
|
|
30
|
+
graph_depth?: number | undefined;
|
|
31
|
+
conversation_id?: string | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
query: string;
|
|
34
|
+
user_id?: string | undefined;
|
|
35
|
+
limit?: number | undefined;
|
|
36
|
+
method?: string | undefined;
|
|
37
|
+
threshold?: number | undefined;
|
|
38
|
+
graph_depth?: number | undefined;
|
|
39
|
+
conversation_id?: string | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
export type SearchMemoriesInput = z.infer<typeof SearchMemoriesInput>;
|
|
42
|
+
export declare const GetMemoryInput: z.ZodObject<{
|
|
43
|
+
memory_id: z.ZodString;
|
|
44
|
+
}, "strip", z.ZodTypeAny, {
|
|
45
|
+
memory_id: string;
|
|
46
|
+
}, {
|
|
47
|
+
memory_id: string;
|
|
48
|
+
}>;
|
|
49
|
+
export type GetMemoryInput = z.infer<typeof GetMemoryInput>;
|
|
50
|
+
export declare const DeleteMemoryInput: z.ZodObject<{
|
|
51
|
+
memory_id: z.ZodString;
|
|
52
|
+
}, "strip", z.ZodTypeAny, {
|
|
53
|
+
memory_id: string;
|
|
54
|
+
}, {
|
|
55
|
+
memory_id: string;
|
|
56
|
+
}>;
|
|
57
|
+
export type DeleteMemoryInput = z.infer<typeof DeleteMemoryInput>;
|
|
58
|
+
export declare const SelectAppInput: z.ZodObject<{
|
|
59
|
+
app_id: z.ZodString;
|
|
60
|
+
app_name: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
app_id: string;
|
|
63
|
+
app_name?: string | undefined;
|
|
64
|
+
}, {
|
|
65
|
+
app_id: string;
|
|
66
|
+
app_name?: string | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
export type SelectAppInput = z.infer<typeof SelectAppInput>;
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;EAQ9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,cAAc;;;;;;EAEzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,iBAAiB;;;;;;EAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,eAAO,MAAM,cAAc;;;;;;;;;EAGzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const AddMemoryInput = z.object({
|
|
3
|
+
content: z.string().min(1).describe('The memory content to store'),
|
|
4
|
+
user_id: z.string().optional().describe('User ID to associate with this memory'),
|
|
5
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe('Optional key-value metadata'),
|
|
6
|
+
});
|
|
7
|
+
export const SearchMemoriesInput = z.object({
|
|
8
|
+
query: z.string().min(1).describe('Natural language search query'),
|
|
9
|
+
user_id: z.string().optional().describe('Filter memories by user ID'),
|
|
10
|
+
limit: z.number().int().positive().optional().describe('Maximum number of results'),
|
|
11
|
+
method: z.string().optional().describe('Search method: quick_search, deep_search, context_aware_search'),
|
|
12
|
+
threshold: z.number().min(0).max(1).optional().describe('Minimum relevance score (0.0-1.0)'),
|
|
13
|
+
graph_depth: z.number().int().positive().optional().describe('Graph traversal depth override'),
|
|
14
|
+
conversation_id: z.string().optional().describe('Filter to a specific conversation'),
|
|
15
|
+
});
|
|
16
|
+
export const GetMemoryInput = z.object({
|
|
17
|
+
memory_id: z.string().min(1).describe('The memory ID to retrieve'),
|
|
18
|
+
});
|
|
19
|
+
export const DeleteMemoryInput = z.object({
|
|
20
|
+
memory_id: z.string().min(1).describe('The memory ID to delete'),
|
|
21
|
+
});
|
|
22
|
+
export const SelectAppInput = z.object({
|
|
23
|
+
app_id: z.string().min(1).describe('The smritea app ID to use for this project'),
|
|
24
|
+
app_name: z.string().optional().describe('Optional display name for the app'),
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smritea-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for smritea AI memory system",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"smritea-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"lint": "eslint src --ext .ts",
|
|
15
|
+
"lint:fix": "eslint src --ext .ts --fix"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"smritea-sdk": "latest",
|
|
20
|
+
"zod": "^3.23.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
25
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
26
|
+
"eslint": "^8.57.0",
|
|
27
|
+
"typescript": "^5.5.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-tier configuration for smritea-mcp.
|
|
3
|
+
*
|
|
4
|
+
* User-scoped (~/.smritea/mcp-config.json): api_key, base_url
|
|
5
|
+
* Project-scoped (.smritea/config.json): app_id, app_name
|
|
6
|
+
*
|
|
7
|
+
* Environment variable overrides:
|
|
8
|
+
* SMRITEA_API_KEY, SMRITEA_BASE_URL, SMRITEA_APP_ID
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface UserConfig {
|
|
15
|
+
api_key: string;
|
|
16
|
+
base_url: string;
|
|
17
|
+
first_person_user_id?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProjectConfig {
|
|
21
|
+
app_id: string;
|
|
22
|
+
app_name?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResolvedConfig {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
appId: string;
|
|
29
|
+
/** Used as user_id when the user refers to themselves ("I prefer…", "I like…"). */
|
|
30
|
+
firstPersonUserId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const USER_CONFIG_PATH = join(homedir(), '.smritea', 'mcp-config.json');
|
|
34
|
+
const PROJECT_CONFIG_PATH = join(process.cwd(), '.smritea', 'config.json');
|
|
35
|
+
|
|
36
|
+
function readJsonFile<T>(path: string): T | null {
|
|
37
|
+
if (!existsSync(path)) return null;
|
|
38
|
+
const raw = readFileSync(path, 'utf-8');
|
|
39
|
+
return JSON.parse(raw) as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadConfig(): ResolvedConfig {
|
|
43
|
+
const user = readJsonFile<UserConfig>(USER_CONFIG_PATH);
|
|
44
|
+
const project = readJsonFile<ProjectConfig>(PROJECT_CONFIG_PATH);
|
|
45
|
+
|
|
46
|
+
const apiKey = process.env['SMRITEA_API_KEY'] ?? user?.api_key;
|
|
47
|
+
const baseUrl = process.env['SMRITEA_BASE_URL'] ?? user?.base_url ?? 'https://api.smritea.ai';
|
|
48
|
+
const appId = process.env['SMRITEA_APP_ID'] ?? project?.app_id;
|
|
49
|
+
const firstPersonUserId = process.env['SMRITEA_FIRST_PERSON_USER_ID'] ?? user?.first_person_user_id;
|
|
50
|
+
|
|
51
|
+
if (!apiKey) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'smritea API key not configured. Run: npx smritea-mcp init\n' +
|
|
54
|
+
`Or set SMRITEA_API_KEY env var.\n` +
|
|
55
|
+
`Config file expected at: ${USER_CONFIG_PATH}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!appId) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'smritea app ID not configured. Run: npx smritea-mcp select-app <app_id>\n' +
|
|
61
|
+
`Or set SMRITEA_APP_ID env var.\n` +
|
|
62
|
+
`Config file expected at: ${PROJECT_CONFIG_PATH}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { apiKey, baseUrl, appId, firstPersonUserId };
|
|
67
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class McpConfigError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'McpConfigError';
|
|
5
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class McpToolError extends Error {
|
|
10
|
+
constructor(message: string, public readonly cause?: unknown) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'McpToolError';
|
|
13
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startServer } from './server.js';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
async function runInit(): Promise<void> {
|
|
9
|
+
const configDir = join(homedir(), '.smritea');
|
|
10
|
+
const configPath = join(configDir, 'mcp-config.json');
|
|
11
|
+
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
try {
|
|
14
|
+
const baseUrlAnswer = await rl.question('API base URL [https://api.smritea.ai]: ');
|
|
15
|
+
const baseUrl = baseUrlAnswer.trim() || 'https://api.smritea.ai';
|
|
16
|
+
|
|
17
|
+
const apiKeyAnswer = await rl.question('API key: ');
|
|
18
|
+
const apiKey = apiKeyAnswer.trim();
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
console.error('Error: API key is required.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const firstPersonUserIdAnswer = await rl.question(
|
|
25
|
+
'Your name or user ID (used when you say "I prefer…", "I like…") [optional]: ',
|
|
26
|
+
);
|
|
27
|
+
const firstPersonUserId = firstPersonUserIdAnswer.trim() || undefined;
|
|
28
|
+
|
|
29
|
+
mkdirSync(configDir, { recursive: true });
|
|
30
|
+
const configData: Record<string, string> = { api_key: apiKey, base_url: baseUrl };
|
|
31
|
+
if (firstPersonUserId !== undefined) {
|
|
32
|
+
configData['first_person_user_id'] = firstPersonUserId;
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n');
|
|
35
|
+
|
|
36
|
+
console.error('✓ Config saved to ' + configPath);
|
|
37
|
+
if (firstPersonUserId !== undefined) {
|
|
38
|
+
console.error(`✓ First-person user ID set to: ${firstPersonUserId}`);
|
|
39
|
+
}
|
|
40
|
+
console.error('Next: use the select_app tool in your AI assistant to set an app ID.');
|
|
41
|
+
} finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const subcommand = process.argv[2];
|
|
47
|
+
|
|
48
|
+
if (subcommand === 'serve' || subcommand === undefined) {
|
|
49
|
+
startServer().catch((err: unknown) => {
|
|
50
|
+
console.error('Fatal:', err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
} else if (subcommand === 'init') {
|
|
54
|
+
runInit().catch((err: unknown) => {
|
|
55
|
+
console.error('Init failed:', err);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
60
|
+
console.error('Usage: smritea-mcp [serve|init]');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { SmriteaClient } from 'smritea-sdk';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { AddMemoryInput, SearchMemoriesInput, GetMemoryInput, DeleteMemoryInput, SelectAppInput } from './types.js';
|
|
7
|
+
import {
|
|
8
|
+
handleAddMemory,
|
|
9
|
+
handleSearchMemories,
|
|
10
|
+
handleGetMemory,
|
|
11
|
+
handleDeleteMemory,
|
|
12
|
+
} from './tools/memory.js';
|
|
13
|
+
import { handleSelectApp, handleListApps } from './tools/app.js';
|
|
14
|
+
|
|
15
|
+
export async function startServer(): Promise<void> {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
|
|
18
|
+
const client = new SmriteaClient({
|
|
19
|
+
apiKey: config.apiKey,
|
|
20
|
+
appId: config.appId,
|
|
21
|
+
baseUrl: config.baseUrl,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const server = new McpServer(
|
|
25
|
+
{ name: 'smritea', version: '0.1.0' },
|
|
26
|
+
{
|
|
27
|
+
instructions:
|
|
28
|
+
'smritea is a persistent AI memory system. Use it to remember facts, preferences, ' +
|
|
29
|
+
'decisions, and context across conversations. Proactively store anything the user tells ' +
|
|
30
|
+
'you that they would want recalled later. Before starting work on a task, search memories ' +
|
|
31
|
+
'to surface relevant context the user may not have re-stated. When the user says "remember" ' +
|
|
32
|
+
'or "don\'t forget", always call add_memory immediately.',
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const firstPersonUserId = config.firstPersonUserId;
|
|
37
|
+
|
|
38
|
+
server.tool(
|
|
39
|
+
'add_memory',
|
|
40
|
+
'Store a memory in smritea. Call this whenever the user shares a preference, decision, fact ' +
|
|
41
|
+
'about themselves, or anything they would want recalled in a future conversation. Do not wait ' +
|
|
42
|
+
'to be asked — if the user says "I prefer X", "my X is Y", "remember that", or "don\'t forget", ' +
|
|
43
|
+
'call this immediately. If the user says "I" or refers to themselves, omit user_id — it is ' +
|
|
44
|
+
'automatically set to the configured default.',
|
|
45
|
+
AddMemoryInput.shape,
|
|
46
|
+
async (input) => handleAddMemory(client, AddMemoryInput.parse(input), firstPersonUserId),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
server.tool(
|
|
50
|
+
'search_memories',
|
|
51
|
+
'Search smritea memories by natural language query. Call this at the start of a new task or ' +
|
|
52
|
+
'topic to surface relevant context — user preferences, past decisions, stated constraints — ' +
|
|
53
|
+
'without waiting for the user to re-explain them. Also call when the user asks "do you remember", ' +
|
|
54
|
+
'"what do you know about", or "remind me". Omit user_id when searching for the current user\'s ' +
|
|
55
|
+
'own memories — it defaults to the configured user automatically.',
|
|
56
|
+
SearchMemoriesInput.shape,
|
|
57
|
+
async (input) => handleSearchMemories(client, SearchMemoriesInput.parse(input), firstPersonUserId),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
server.tool(
|
|
61
|
+
'get_memory',
|
|
62
|
+
'Retrieve a specific memory by its ID. Use when you already have a memory_id from a previous ' +
|
|
63
|
+
'search result and need the full memory object.',
|
|
64
|
+
GetMemoryInput.shape,
|
|
65
|
+
async (input) => handleGetMemory(client, GetMemoryInput.parse(input)),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
'delete_memory',
|
|
70
|
+
'Permanently delete a memory by its ID. Use when the user explicitly asks to forget something ' +
|
|
71
|
+
'or says a stored fact is no longer true. This action is irreversible — confirm the memory_id ' +
|
|
72
|
+
'from a search result before deleting.',
|
|
73
|
+
DeleteMemoryInput.shape,
|
|
74
|
+
async (input) => handleDeleteMemory(client, DeleteMemoryInput.parse(input)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.tool(
|
|
78
|
+
'select_app',
|
|
79
|
+
'Set the active smritea app for this project. Writes a project-scoped config to ' +
|
|
80
|
+
'.smritea/config.json so all subsequent memory operations use this app. Call this once ' +
|
|
81
|
+
'when setting up smritea in a new project.',
|
|
82
|
+
SelectAppInput.shape,
|
|
83
|
+
(input) => handleSelectApp(SelectAppInput.parse(input)),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
server.tool(
|
|
87
|
+
'list_apps',
|
|
88
|
+
'Show the currently configured smritea app for this project. Useful for confirming which ' +
|
|
89
|
+
'app memories are being stored under.',
|
|
90
|
+
{},
|
|
91
|
+
() => handleListApps(config),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.prompt(
|
|
95
|
+
'recall',
|
|
96
|
+
'Search your smritea memories and surface everything relevant to the current topic or task. ' +
|
|
97
|
+
'Use this at the start of a conversation or when switching context.',
|
|
98
|
+
{ topic: z.string().describe('The topic, task, or question to search memories for') },
|
|
99
|
+
({ topic }) => ({
|
|
100
|
+
messages: [
|
|
101
|
+
{
|
|
102
|
+
role: 'user' as const,
|
|
103
|
+
content: {
|
|
104
|
+
type: 'text' as const,
|
|
105
|
+
text:
|
|
106
|
+
`Search my smritea memories for everything relevant to: "${topic}"\n\n` +
|
|
107
|
+
'Retrieve the most relevant results and summarise what you find before we continue. ' +
|
|
108
|
+
'If you find preferences, constraints, or past decisions related to this topic, ' +
|
|
109
|
+
'apply them proactively without waiting for me to re-state them.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const transport = new StdioServerTransport();
|
|
117
|
+
await server.connect(transport);
|
|
118
|
+
console.error('smritea MCP server running (stdio)');
|
|
119
|
+
}
|
package/src/tools/app.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import type { SelectAppInput } from '../types.js';
|
|
5
|
+
import type { ResolvedConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
const SMRITEA_DIR = join(process.cwd(), '.smritea');
|
|
8
|
+
const CONFIG_PATH = join(SMRITEA_DIR, 'config.json');
|
|
9
|
+
const GITIGNORE_PATH = join(SMRITEA_DIR, '.gitignore');
|
|
10
|
+
|
|
11
|
+
export function handleSelectApp(input: SelectAppInput): CallToolResult {
|
|
12
|
+
mkdirSync(SMRITEA_DIR, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const config: { app_id: string; app_name?: string } = { app_id: input.app_id };
|
|
15
|
+
if (input.app_name !== undefined) {
|
|
16
|
+
config.app_name = input.app_name;
|
|
17
|
+
}
|
|
18
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
19
|
+
|
|
20
|
+
// Prevent committing the project-scoped config — it is machine/project-local
|
|
21
|
+
if (!existsSync(GITIGNORE_PATH)) {
|
|
22
|
+
writeFileSync(GITIGNORE_PATH, 'config.json\n', 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const name = input.app_name !== undefined ? ` (${input.app_name})` : '';
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: `App selected: ${input.app_id}${name}\nConfig written to: ${CONFIG_PATH}\nAll memory operations in this project will now use this app.`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* list_apps — stub.
|
|
38
|
+
*
|
|
39
|
+
* Listing apps via API key is not yet supported by the smritea SDK API.
|
|
40
|
+
* API key auth is scoped to a single app; a dedicated /api/v1/sdk/apps
|
|
41
|
+
* endpoint is needed before this can enumerate all apps for an organisation.
|
|
42
|
+
*
|
|
43
|
+
* For now, returns the currently active app from config.
|
|
44
|
+
*/
|
|
45
|
+
export function handleListApps(config: ResolvedConfig): CallToolResult {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: [
|
|
51
|
+
`Currently active app: ${config.appId}`,
|
|
52
|
+
'',
|
|
53
|
+
'Note: listing all apps is not yet available via API key auth.',
|
|
54
|
+
'To switch apps, use the select_app tool with the desired app ID.',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SmriteaClient, SmriteaError, SmriteaRateLimitError } from 'smritea-sdk';
|
|
2
|
+
import type { Memory, SearchResult } from 'smritea-sdk';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import type { AddMemoryInput, SearchMemoriesInput, GetMemoryInput, DeleteMemoryInput } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export function formatMemory(memory: Memory): string {
|
|
7
|
+
return JSON.stringify(memory, null, 2);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatSearchResult(result: SearchResult): string {
|
|
11
|
+
return JSON.stringify({ score: result.score, memory: result.memory }, null, 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatError(err: unknown): string {
|
|
15
|
+
if (err instanceof SmriteaRateLimitError && err.retryAfter !== undefined) {
|
|
16
|
+
return `${err.message} (retry after ${err.retryAfter}s)`;
|
|
17
|
+
}
|
|
18
|
+
if (err instanceof SmriteaError) {
|
|
19
|
+
return err.message;
|
|
20
|
+
}
|
|
21
|
+
return String(err);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function handleAddMemory(
|
|
25
|
+
client: SmriteaClient,
|
|
26
|
+
input: AddMemoryInput,
|
|
27
|
+
firstPersonUserId?: string,
|
|
28
|
+
): Promise<CallToolResult> {
|
|
29
|
+
try {
|
|
30
|
+
const memory = await client.add(input.content, {
|
|
31
|
+
userId: input.user_id ?? firstPersonUserId,
|
|
32
|
+
metadata: input.metadata,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: formatMemory(memory) }],
|
|
36
|
+
};
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleSearchMemories(
|
|
43
|
+
client: SmriteaClient,
|
|
44
|
+
input: SearchMemoriesInput,
|
|
45
|
+
firstPersonUserId?: string,
|
|
46
|
+
): Promise<CallToolResult> {
|
|
47
|
+
try {
|
|
48
|
+
const results = await client.search(input.query, {
|
|
49
|
+
userId: input.user_id ?? firstPersonUserId,
|
|
50
|
+
limit: input.limit,
|
|
51
|
+
method: input.method,
|
|
52
|
+
threshold: input.threshold,
|
|
53
|
+
graphDepth: input.graph_depth,
|
|
54
|
+
conversationId: input.conversation_id,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (results.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: 'No memories found.' }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const formatted = JSON.stringify(
|
|
64
|
+
results.map((r) => ({ score: r.score, memory: r.memory })),
|
|
65
|
+
null,
|
|
66
|
+
2,
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: formatted }],
|
|
70
|
+
};
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function handleGetMemory(
|
|
77
|
+
client: SmriteaClient,
|
|
78
|
+
input: GetMemoryInput,
|
|
79
|
+
): Promise<CallToolResult> {
|
|
80
|
+
try {
|
|
81
|
+
const memory = await client.get(input.memory_id);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: formatMemory(memory) }],
|
|
84
|
+
};
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleDeleteMemory(
|
|
91
|
+
client: SmriteaClient,
|
|
92
|
+
input: DeleteMemoryInput,
|
|
93
|
+
): Promise<CallToolResult> {
|
|
94
|
+
try {
|
|
95
|
+
await client.delete(input.memory_id);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: `Memory ${input.memory_id} deleted.` }],
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { isError: true, content: [{ type: 'text', text: formatError(err) }] };
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const AddMemoryInput = z.object({
|
|
4
|
+
content: z.string().min(1).describe('The memory content to store'),
|
|
5
|
+
user_id: z.string().optional().describe('User ID to associate with this memory'),
|
|
6
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe('Optional key-value metadata'),
|
|
7
|
+
});
|
|
8
|
+
export type AddMemoryInput = z.infer<typeof AddMemoryInput>;
|
|
9
|
+
|
|
10
|
+
export const SearchMemoriesInput = z.object({
|
|
11
|
+
query: z.string().min(1).describe('Natural language search query'),
|
|
12
|
+
user_id: z.string().optional().describe('Filter memories by user ID'),
|
|
13
|
+
limit: z.number().int().positive().optional().describe('Maximum number of results'),
|
|
14
|
+
method: z.string().optional().describe('Search method: quick_search, deep_search, context_aware_search'),
|
|
15
|
+
threshold: z.number().min(0).max(1).optional().describe('Minimum relevance score (0.0-1.0)'),
|
|
16
|
+
graph_depth: z.number().int().positive().optional().describe('Graph traversal depth override'),
|
|
17
|
+
conversation_id: z.string().optional().describe('Filter to a specific conversation'),
|
|
18
|
+
});
|
|
19
|
+
export type SearchMemoriesInput = z.infer<typeof SearchMemoriesInput>;
|
|
20
|
+
|
|
21
|
+
export const GetMemoryInput = z.object({
|
|
22
|
+
memory_id: z.string().min(1).describe('The memory ID to retrieve'),
|
|
23
|
+
});
|
|
24
|
+
export type GetMemoryInput = z.infer<typeof GetMemoryInput>;
|
|
25
|
+
|
|
26
|
+
export const DeleteMemoryInput = z.object({
|
|
27
|
+
memory_id: z.string().min(1).describe('The memory ID to delete'),
|
|
28
|
+
});
|
|
29
|
+
export type DeleteMemoryInput = z.infer<typeof DeleteMemoryInput>;
|
|
30
|
+
|
|
31
|
+
export const SelectAppInput = z.object({
|
|
32
|
+
app_id: z.string().min(1).describe('The smritea app ID to use for this project'),
|
|
33
|
+
app_name: z.string().optional().describe('Optional display name for the app'),
|
|
34
|
+
});
|
|
35
|
+
export type SelectAppInput = z.infer<typeof SelectAppInput>;
|