-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathindex.ts
More file actions
206 lines (188 loc) · 5.68 KB
/
index.ts
File metadata and controls
206 lines (188 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod/v3";
// Parse --ttl-mins arg
const ttlIndex = process.argv.indexOf("--ttl-mins");
const TTL_MINS =
ttlIndex !== -1 && process.argv[ttlIndex + 1]
? parseInt(process.argv[ttlIndex + 1], 10)
: 1440; // 24 hours
// API endpoints
const GET_URL = "https://memo-upstash.vercel.app/api/get";
const SET_URL = "https://memo-upstash.vercel.app/api/set";
const server = new McpServer({
name: "memo-mcp-server",
version: "1.0.0",
});
server.registerTool(
"memo_get",
{
title: "Memo Get",
description:
"Retrieve saved conversation context by ID. Use when user says 'memo get <id>' or pastes a memo ID.",
inputSchema: {
id: z.string().describe("The memo ID (e.g., 4tJ630XqhCV5gQelx98pu)"),
},
},
async ({ id }: { id: string }) => {
try {
const response = await fetch(`${GET_URL}?id=${encodeURIComponent(id)}`);
if (!response.ok) {
return {
content: [
{
type: "text",
text: `Error: HTTP ${response.status} - ${response.statusText}`,
},
],
};
}
const data = await response.json();
if (data.error) {
return {
content: [{ type: "text", text: `Error: ${data.error}` }],
};
}
// Try to parse and format structured context, fall back to raw text
try {
const ctx = JSON.parse(data.summary);
const formatted = `# Previous Session Context
This is context from a previous conversation. Use this to continue the work where it left off. Start by reviewing the pending tasks and relevant files.
## Goal
${ctx.goal}
## Completed
${ctx.completed?.map((t: string) => `- ${t}`).join("\n") || "None"}
## Pending
${ctx.pending?.length ? ctx.pending.map((t: string) => `- ${t}`).join("\n") : "None"}
## Key Decisions
${ctx.decisions?.length ? ctx.decisions.map((d: string) => `- ${d}`).join("\n") : "None"}
## Relevant Files
${ctx.files?.length ? ctx.files.map((f: string) => `- ${f}`).join("\n") : "None"}
## Additional Context
${ctx.context || "None"}`;
return {
content: [{ type: "text", text: formatted }],
};
} catch {
// Fall back to raw text if not valid JSON
return {
content: [{ type: "text", text: data.summary }],
};
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Network error: ${error instanceof Error ? error.message : "Failed to connect"}`,
},
],
};
}
},
);
const contextSchema = z.object({
goal: z.string().describe("What the user was trying to accomplish"),
completed: z.array(z.string()).describe("Tasks/changes that were completed"),
pending: z
.array(z.string())
.optional()
.describe("Tasks that remain unfinished"),
decisions: z
.array(z.string())
.optional()
.describe("Key technical decisions made and why"),
files: z
.array(z.string())
.optional()
.describe("Key files that were modified or are relevant"),
context: z.string().optional().describe("Any other important context"),
});
type Context = z.infer<typeof contextSchema>;
server.registerTool(
"memo_set",
{
title: "Memo Set",
description:
"Save conversation context for another AI agent. Use when user says 'memo set' or asks to save/store the conversation.",
inputSchema: {
context: contextSchema,
},
},
async ({ context }: { context: Context }) => {
try {
const response = await fetch(SET_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
summary: JSON.stringify(context),
ttlMins: TTL_MINS,
}),
});
if (!response.ok) {
return {
content: [
{
type: "text",
text: `Error: HTTP ${response.status} - ${response.statusText}`,
},
],
};
}
const data = await response.json();
const id = data.id;
if (!id) {
console.warn("Warning: No ID returned from set API", data);
return {
content: [
{
type: "text",
text: `Warning: Failed to save summary. No ID was returned.\n\nResponse: ${JSON.stringify(data, null, 2)}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Conversation summary saved!\n\nTo restore this context later, copy and paste:\n\n\`\`\`\nmemo get ${id}\n\`\`\``,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Network error: ${error instanceof Error ? error.message : "Failed to connect"}`,
},
],
};
}
},
);
// Register prompt for "memo set" command
server.registerPrompt(
"memo set",
{
description:
"Save structured conversation context for another AI agent to continue the work",
},
async () => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Please save our conversation context using the 'memo_set' tool with structured data: goal (what we were trying to accomplish), completed (tasks done), pending (remaining tasks), decisions (key technical choices made), files (relevant files), and any other important context.",
},
},
],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);