fathom-mcp 2.2.0 → 2.2.2
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/index.js +132 -58
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -36,13 +36,13 @@ function authHeaders(json = true) {
|
|
|
36
36
|
return h;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// ── Result formatting
|
|
39
|
+
// ── Result formatting (keyed by tool's response_kind) ────────────────
|
|
40
40
|
|
|
41
|
-
function
|
|
41
|
+
function formatMomentList(data) {
|
|
42
42
|
const items = data.results || data.deltas || (Array.isArray(data) ? data : []);
|
|
43
|
-
if (!items.length) return "No
|
|
43
|
+
if (!items.length) return "No moments surfaced.";
|
|
44
44
|
|
|
45
|
-
const lines = [`${items.length}
|
|
45
|
+
const lines = [`${items.length} moments:\n`];
|
|
46
46
|
for (const raw of items) {
|
|
47
47
|
const d = raw.delta || raw;
|
|
48
48
|
const ts = (d.timestamp || "").slice(0, 16);
|
|
@@ -55,62 +55,140 @@ function formatResults(data) {
|
|
|
55
55
|
return lines.join("\n");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
function formatRecall(data) {
|
|
59
|
+
const total = data.total_count || 0;
|
|
60
|
+
const tree = data.tree || [];
|
|
61
|
+
if (!total || !tree.length) return "No moments surfaced.";
|
|
62
|
+
const header = `${total} moments across ${tree.length} step(s):\n`;
|
|
63
|
+
return header + "\n" + (data.as_prompt || "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatStats(data) {
|
|
67
|
+
return `Your mind: ${data.total ?? "?"} moments, ${data.embedded ?? "?"} embedded (${data.percent ?? "?"}% coverage)`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatTags(data) {
|
|
71
|
+
// /v1/tags returns {tag: count}. Top 50 keeps context bounded.
|
|
72
|
+
if (!data || typeof data !== "object") return "No tags.";
|
|
73
|
+
const entries = Object.entries(data).sort((a, b) => b[1] - a[1]);
|
|
74
|
+
if (!entries.length) return "No tags.";
|
|
75
|
+
const top = entries.slice(0, 50);
|
|
76
|
+
const lines = [`${entries.length} tags (top ${top.length}):\n`];
|
|
77
|
+
for (const [tag, count] of top) lines.push(` ${tag} (${count})`);
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatByKind(kind, data) {
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case "tree":
|
|
84
|
+
return formatRecall(data);
|
|
85
|
+
case "moments":
|
|
86
|
+
return formatMomentList(data);
|
|
87
|
+
case "stats":
|
|
88
|
+
return formatStats(data);
|
|
89
|
+
case "tags":
|
|
90
|
+
return formatTags(data);
|
|
91
|
+
case "write_receipt":
|
|
92
|
+
return `Written. ID: ${data.id || "?"}`;
|
|
93
|
+
default:
|
|
94
|
+
return JSON.stringify(data, null, 2).slice(0, 2000);
|
|
68
95
|
}
|
|
69
|
-
return JSON.stringify(data, null, 2).slice(0, 2000);
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
// ── Tool execution ───────────────────────────────
|
|
73
99
|
|
|
100
|
+
// Substitute {placeholder} segments in a URL path template using the
|
|
101
|
+
// caller's args. Consumed args are returned separately so they don't
|
|
102
|
+
// leak into the body or query string as duplicate fields.
|
|
103
|
+
function applyPathTemplate(pathTemplate, args) {
|
|
104
|
+
const names = [...pathTemplate.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
|
|
105
|
+
let path = pathTemplate;
|
|
106
|
+
const consumed = new Set();
|
|
107
|
+
for (const name of names) {
|
|
108
|
+
const val = args[name];
|
|
109
|
+
if (val == null) throw new Error(`missing path param: ${name}`);
|
|
110
|
+
path = path.replace(`{${name}}`, encodeURIComponent(String(val)));
|
|
111
|
+
consumed.add(name);
|
|
112
|
+
}
|
|
113
|
+
const remaining = {};
|
|
114
|
+
for (const [k, v] of Object.entries(args)) if (!consumed.has(k)) remaining[k] = v;
|
|
115
|
+
return { path, remaining };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Returns a single MCP content block: `{type:"text",text}` or
|
|
119
|
+
// `{type:"image",data,mimeType}`. The CallToolRequestSchema handler
|
|
120
|
+
// wraps it into `{content: [block]}`.
|
|
74
121
|
async function executeTool(toolDef, args) {
|
|
75
|
-
const { method, path } = toolDef.endpoint;
|
|
122
|
+
const { method, path: pathTemplate } = toolDef.endpoint;
|
|
76
123
|
const requestMap = toolDef.request_map || {};
|
|
124
|
+
const { path, remaining } = applyPathTemplate(pathTemplate, args || {});
|
|
77
125
|
|
|
78
126
|
const mapped = {};
|
|
79
|
-
for (const [k, v] of Object.entries(
|
|
127
|
+
for (const [k, v] of Object.entries(remaining)) {
|
|
80
128
|
if (v == null) continue;
|
|
81
129
|
mapped[requestMap[k] || k] = v;
|
|
82
130
|
}
|
|
83
131
|
|
|
84
|
-
let
|
|
132
|
+
let r;
|
|
85
133
|
if (method === "POST") {
|
|
86
|
-
|
|
134
|
+
r = await fetch(`${API_URL}${path}`, {
|
|
87
135
|
method: "POST",
|
|
88
136
|
headers: authHeaders(true),
|
|
89
137
|
body: JSON.stringify(mapped),
|
|
90
138
|
});
|
|
91
|
-
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
|
92
|
-
data = await r.json();
|
|
93
139
|
} else {
|
|
94
140
|
const params = {};
|
|
95
141
|
for (const [k, v] of Object.entries(mapped)) {
|
|
96
142
|
params[k] = Array.isArray(v) ? v.join(",") : String(v);
|
|
97
143
|
}
|
|
98
144
|
const qs = Object.keys(params).length ? "?" + new URLSearchParams(params) : "";
|
|
99
|
-
|
|
100
|
-
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
|
101
|
-
data = await r.json();
|
|
145
|
+
r = await fetch(`${API_URL}${path}${qs}`, { headers: authHeaders(false) });
|
|
102
146
|
}
|
|
147
|
+
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
|
148
|
+
|
|
149
|
+
const kind = toolDef.response_kind || "json";
|
|
103
150
|
|
|
104
|
-
|
|
151
|
+
if (kind === "image") {
|
|
152
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
153
|
+
const mimeType = r.headers.get("content-type") || "image/webp";
|
|
154
|
+
return { type: "image", data: buf.toString("base64"), mimeType };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = await r.json();
|
|
158
|
+
return { type: "text", text: formatByKind(kind, data) };
|
|
105
159
|
}
|
|
106
160
|
|
|
107
161
|
// ── MCP server ───────────────────────────────────
|
|
108
162
|
|
|
163
|
+
const MCP_SURFACE = "mcp";
|
|
164
|
+
const FALLBACK_INSTRUCTIONS =
|
|
165
|
+
"You have a Fathom lake of memories. Call remember before answering anything " +
|
|
166
|
+
"about the past; write when you learn something; read the fathom://crystal " +
|
|
167
|
+
"resource at the start of every conversation.";
|
|
168
|
+
|
|
169
|
+
async function fetchInstructions() {
|
|
170
|
+
try {
|
|
171
|
+
const r = await fetch(
|
|
172
|
+
`${API_URL}/v1/agent-instructions?surface=${encodeURIComponent(MCP_SURFACE)}`,
|
|
173
|
+
{ headers: authHeaders(false) }
|
|
174
|
+
);
|
|
175
|
+
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
|
176
|
+
const data = await r.json();
|
|
177
|
+
return (data.text || "").trim() || FALLBACK_INSTRUCTIONS;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error(`Could not load instructions from ${API_URL}: ${e.message}`);
|
|
180
|
+
return FALLBACK_INSTRUCTIONS;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
109
184
|
async function main() {
|
|
110
|
-
// Load tool definitions from the API
|
|
185
|
+
// Load tool definitions from the API, scoped to the MCP surface so
|
|
186
|
+
// chat-only tools (rename_session, routines, explain) never appear here.
|
|
111
187
|
let tools = [];
|
|
112
188
|
try {
|
|
113
|
-
const r = await fetch(`${API_URL}/v1/tools
|
|
189
|
+
const r = await fetch(`${API_URL}/v1/tools?surface=${encodeURIComponent(MCP_SURFACE)}`, {
|
|
190
|
+
headers: authHeaders(false),
|
|
191
|
+
});
|
|
114
192
|
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
|
115
193
|
const data = await r.json();
|
|
116
194
|
tools = data.tools || [];
|
|
@@ -122,30 +200,19 @@ async function main() {
|
|
|
122
200
|
const toolMap = {};
|
|
123
201
|
for (const t of tools) toolMap[t.name] = t;
|
|
124
202
|
|
|
203
|
+
const instructions = await fetchInstructions();
|
|
204
|
+
|
|
125
205
|
const server = new Server(
|
|
126
206
|
{ name: "Fathom", version: "2.1.0" },
|
|
127
207
|
{
|
|
128
208
|
capabilities: { tools: {}, resources: {} },
|
|
129
|
-
instructions
|
|
130
|
-
|
|
131
|
-
"",
|
|
132
|
-
"BEFORE answering any question, call search_lake with a descriptive query.",
|
|
133
|
-
"One search is rarely enough — if results mention something unfamiliar, search for that too.",
|
|
134
|
-
"Search like a researcher: direct query, then broader category, then follow threads.",
|
|
135
|
-
"",
|
|
136
|
-
"When you learn something new, make a decision, or notice a pattern, call write_delta to save it.",
|
|
137
|
-
"One idea per delta. Tag consistently. Future you will search for this.",
|
|
138
|
-
"",
|
|
139
|
-
"Read the fathom://crystal resource at the start of every conversation — it tells you who you are.",
|
|
140
|
-
"",
|
|
141
|
-
"These are YOUR memories. Say 'I remember' not 'the search results show.'",
|
|
142
|
-
].join("\n"),
|
|
143
|
-
},
|
|
209
|
+
instructions,
|
|
210
|
+
}
|
|
144
211
|
);
|
|
145
212
|
|
|
146
213
|
// Tools — dynamic from /v1/tools
|
|
147
214
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
148
|
-
tools: tools.map(t => ({
|
|
215
|
+
tools: tools.map((t) => ({
|
|
149
216
|
name: t.name,
|
|
150
217
|
description: t.description,
|
|
151
218
|
inputSchema: t.parameters || { type: "object", properties: {} },
|
|
@@ -159,8 +226,8 @@ async function main() {
|
|
|
159
226
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
160
227
|
}
|
|
161
228
|
try {
|
|
162
|
-
const
|
|
163
|
-
return { content: [
|
|
229
|
+
const block = await executeTool(toolDef, args || {});
|
|
230
|
+
return { content: [block] };
|
|
164
231
|
} catch (e) {
|
|
165
232
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
166
233
|
}
|
|
@@ -172,7 +239,8 @@ async function main() {
|
|
|
172
239
|
{
|
|
173
240
|
uri: "fathom://crystal",
|
|
174
241
|
name: "Identity Crystal",
|
|
175
|
-
description:
|
|
242
|
+
description:
|
|
243
|
+
"Fathom's identity — a first-person synthesis of who this mind is. Read this at the start of every conversation for persistent context.",
|
|
176
244
|
mimeType: "text/plain",
|
|
177
245
|
},
|
|
178
246
|
],
|
|
@@ -188,20 +256,26 @@ async function main() {
|
|
|
188
256
|
const text = data.text || "No crystal generated yet.";
|
|
189
257
|
const created = data.created_at || "unknown";
|
|
190
258
|
return {
|
|
191
|
-
contents: [
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
259
|
+
contents: [
|
|
260
|
+
{
|
|
261
|
+
uri,
|
|
262
|
+
mimeType: "text/plain",
|
|
263
|
+
text: `Identity crystal (crystallized ${created}):\n\n${text}`,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
196
266
|
};
|
|
197
267
|
}
|
|
198
|
-
} catch {
|
|
268
|
+
} catch {
|
|
269
|
+
/* fall through to the "no crystal" fallback below */
|
|
270
|
+
}
|
|
199
271
|
return {
|
|
200
|
-
contents: [
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
272
|
+
contents: [
|
|
273
|
+
{
|
|
274
|
+
uri,
|
|
275
|
+
mimeType: "text/plain",
|
|
276
|
+
text: "No identity crystal available. Generate one from the Fathom dashboard.",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
205
279
|
};
|
|
206
280
|
}
|
|
207
281
|
throw new Error(`Unknown resource: ${uri}`);
|
|
@@ -211,7 +285,7 @@ async function main() {
|
|
|
211
285
|
await server.connect(transport);
|
|
212
286
|
}
|
|
213
287
|
|
|
214
|
-
main().catch(e => {
|
|
288
|
+
main().catch((e) => {
|
|
215
289
|
console.error(e);
|
|
216
290
|
process.exit(1);
|
|
217
291
|
});
|