@unclick/mcp-server 0.3.0 → 0.3.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/README.md +34 -13
- package/dist/abn-tool.js +1 -1
- package/dist/bgg-tool.js +1 -1
- package/dist/carboninterface-tool.js +1 -1
- package/dist/cards/card.d.ts +9 -0
- package/dist/cards/card.d.ts.map +1 -0
- package/dist/cards/card.js +4 -0
- package/dist/cards/card.js.map +1 -0
- package/dist/cards/search-memory-card.d.ts +11 -0
- package/dist/cards/search-memory-card.d.ts.map +1 -0
- package/dist/cards/search-memory-card.js +75 -0
- package/dist/cards/search-memory-card.js.map +1 -0
- package/dist/cards/search-memory-card.test.d.ts +2 -0
- package/dist/cards/search-memory-card.test.d.ts.map +1 -0
- package/dist/cards/search-memory-card.test.js +59 -0
- package/dist/cards/search-memory-card.test.js.map +1 -0
- package/dist/catalog.js +36 -36
- package/dist/catalog.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +96 -6
- package/dist/client.js.map +1 -1
- package/dist/converter-tools.js +1 -1
- package/dist/crews-tool.d.ts +12 -0
- package/dist/crews-tool.d.ts.map +1 -0
- package/dist/crews-tool.js +125 -0
- package/dist/crews-tool.js.map +1 -0
- package/dist/gdelt-tool.js +4 -4
- package/dist/hackernews-tool.js +1 -1
- package/dist/line-tool.js +1 -1
- package/dist/local-catalog-handlers.js +1 -1
- package/dist/local-catalog-handlers.js.map +1 -1
- package/dist/local-tools.js +7 -7
- package/dist/local-tools.js.map +1 -1
- package/dist/memory/__tests__/bitemporal.test.d.ts +8 -0
- package/dist/memory/__tests__/bitemporal.test.d.ts.map +1 -0
- package/dist/memory/__tests__/bitemporal.test.js +148 -0
- package/dist/memory/__tests__/bitemporal.test.js.map +1 -0
- package/dist/memory/__tests__/hybrid-search.test.d.ts +14 -0
- package/dist/memory/__tests__/hybrid-search.test.d.ts.map +1 -0
- package/dist/memory/__tests__/hybrid-search.test.js +304 -0
- package/dist/memory/__tests__/hybrid-search.test.js.map +1 -0
- package/dist/memory/agent.d.ts +34 -0
- package/dist/memory/agent.d.ts.map +1 -0
- package/dist/memory/agent.js +69 -0
- package/dist/memory/agent.js.map +1 -0
- package/dist/memory/conflicts.d.ts +48 -0
- package/dist/memory/conflicts.d.ts.map +1 -0
- package/dist/memory/conflicts.js +209 -0
- package/dist/memory/conflicts.js.map +1 -0
- package/dist/memory/db.d.ts +18 -3
- package/dist/memory/db.d.ts.map +1 -1
- package/dist/memory/db.js +133 -11
- package/dist/memory/db.js.map +1 -1
- package/dist/memory/device.d.ts +20 -0
- package/dist/memory/device.d.ts.map +1 -0
- package/dist/memory/device.js +48 -0
- package/dist/memory/device.js.map +1 -0
- package/dist/memory/embeddings.d.ts +10 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +40 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/handlers.d.ts.map +1 -1
- package/dist/memory/handlers.js +98 -4
- package/dist/memory/handlers.js.map +1 -1
- package/dist/memory/instrumentation.d.ts +38 -0
- package/dist/memory/instrumentation.d.ts.map +1 -0
- package/dist/memory/instrumentation.js +97 -0
- package/dist/memory/instrumentation.js.map +1 -0
- package/dist/memory/load-events.d.ts +18 -0
- package/dist/memory/load-events.d.ts.map +1 -0
- package/dist/memory/load-events.js +61 -0
- package/dist/memory/load-events.js.map +1 -0
- package/dist/memory/local.d.ts +4 -1
- package/dist/memory/local.d.ts.map +1 -1
- package/dist/memory/local.js +14 -0
- package/dist/memory/local.js.map +1 -1
- package/dist/memory/session-state.d.ts +37 -0
- package/dist/memory/session-state.d.ts.map +1 -0
- package/dist/memory/session-state.js +82 -0
- package/dist/memory/session-state.js.map +1 -0
- package/dist/memory/supabase.d.ts +75 -5
- package/dist/memory/supabase.d.ts.map +1 -1
- package/dist/memory/supabase.js +584 -83
- package/dist/memory/supabase.js.map +1 -1
- package/dist/memory/tenant-settings.d.ts +33 -0
- package/dist/memory/tenant-settings.d.ts.map +1 -0
- package/dist/memory/tenant-settings.js +79 -0
- package/dist/memory/tenant-settings.js.map +1 -0
- package/dist/memory/tool-awareness.d.ts +66 -0
- package/dist/memory/tool-awareness.d.ts.map +1 -0
- package/dist/memory/tool-awareness.js +307 -0
- package/dist/memory/tool-awareness.js.map +1 -0
- package/dist/memory/types.d.ts +18 -2
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/numbers-tool.js +2 -2
- package/dist/openfoodfacts-tool.js +1 -1
- package/dist/openmeteo-tool.js +1 -1
- package/dist/radiobrowser-tool.js +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +779 -55
- package/dist/server.js.map +1 -1
- package/dist/signals/emit.d.ts +11 -0
- package/dist/signals/emit.d.ts.map +1 -0
- package/dist/signals/emit.js +26 -0
- package/dist/signals/emit.js.map +1 -0
- package/dist/testpass-tool.d.ts +12 -0
- package/dist/testpass-tool.d.ts.map +1 -0
- package/dist/testpass-tool.js +121 -0
- package/dist/testpass-tool.js.map +1 -0
- package/dist/tool-wiring.d.ts +320 -4
- package/dist/tool-wiring.d.ts.map +1 -1
- package/dist/tool-wiring.js +246 -5
- package/dist/tool-wiring.js.map +1 -1
- package/dist/trivia-tool.js +5 -5
- package/dist/usgs-tool.js +1 -1
- package/dist/uxpass-tool.d.ts +24 -0
- package/dist/uxpass-tool.d.ts.map +1 -0
- package/dist/uxpass-tool.js +165 -0
- package/dist/uxpass-tool.js.map +1 -0
- package/dist/vault-bridge.js +7 -7
- package/dist/vercel-tool.d.ts +3 -0
- package/dist/vercel-tool.d.ts.map +1 -1
- package/dist/vercel-tool.js +198 -7
- package/dist/vercel-tool.js.map +1 -1
- package/dist/web-tools.d.ts +62 -0
- package/dist/web-tools.d.ts.map +1 -0
- package/dist/web-tools.js +271 -0
- package/dist/web-tools.js.map +1 -0
- package/package.json +6 -3
- package/server.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3,9 +3,82 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { CATALOG, TOOL_MAP, ENDPOINT_MAP } from "./catalog.js";
|
|
5
5
|
import { createClient } from "./client.js";
|
|
6
|
-
import { ADDITIONAL_HANDLERS } from "./tool-wiring.js";
|
|
6
|
+
import { ADDITIONAL_TOOLS, ADDITIONAL_HANDLERS } from "./tool-wiring.js";
|
|
7
7
|
import { LOCAL_CATALOG_HANDLERS } from "./local-catalog-handlers.js";
|
|
8
8
|
import { MEMORY_HANDLERS } from "./memory/handlers.js";
|
|
9
|
+
import { emitSignal } from "./signals/emit.js";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
// ─── Umami tool-usage tracking ──────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Fires a fire-and-forget event to the self-hosted Umami instance every time
|
|
14
|
+
// an agent actually invokes a tool. Lets Chris see which tools get used.
|
|
15
|
+
// No-ops silently if UMAMI_WEBSITE_ID is not set (e.g. dev / local runs).
|
|
16
|
+
// Never awaited so it cannot slow or break a tool call even if Umami is down.
|
|
17
|
+
function trackToolCall(toolName) {
|
|
18
|
+
const websiteId = process.env.UMAMI_WEBSITE_ID;
|
|
19
|
+
if (!websiteId)
|
|
20
|
+
return;
|
|
21
|
+
const umamiUrl = process.env.UMAMI_URL ?? "https://analytics.unclick.world";
|
|
22
|
+
try {
|
|
23
|
+
void fetch(`${umamiUrl}/api/send`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"User-Agent": "unclick-mcp-server/1.0",
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
type: "event",
|
|
31
|
+
payload: {
|
|
32
|
+
website: websiteId,
|
|
33
|
+
hostname: "unclick.world",
|
|
34
|
+
url: "/api/mcp",
|
|
35
|
+
name: "tool_call",
|
|
36
|
+
data: { tool_name: toolName },
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
}).catch(() => {
|
|
40
|
+
// swallow network / TLS errors
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// swallow synchronous errors (e.g. malformed env)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function currentApiKeyHash() {
|
|
48
|
+
const configuredHash = process.env.UNCLICK_API_KEY_HASH?.trim();
|
|
49
|
+
if (configuredHash)
|
|
50
|
+
return configuredHash;
|
|
51
|
+
const apiKey = process.env.UNCLICK_API_KEY?.trim();
|
|
52
|
+
if (!apiKey)
|
|
53
|
+
return null;
|
|
54
|
+
return createHash("sha256").update(apiKey).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
function failureSummary(toolName, result) {
|
|
57
|
+
if (!result || typeof result !== "object")
|
|
58
|
+
return null;
|
|
59
|
+
const record = result;
|
|
60
|
+
if (typeof record.error === "string" && record.error.trim()) {
|
|
61
|
+
return `${toolName}: ${record.error.trim()}`;
|
|
62
|
+
}
|
|
63
|
+
if (typeof record.headline === "string" && /\bfailed\b/i.test(record.headline)) {
|
|
64
|
+
return `${toolName}: ${record.headline}`;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function signalToolFailure(toolName, result) {
|
|
69
|
+
const apiKeyHash = currentApiKeyHash();
|
|
70
|
+
const summary = failureSummary(toolName, result);
|
|
71
|
+
if (!apiKeyHash || !summary)
|
|
72
|
+
return;
|
|
73
|
+
void emitSignal({
|
|
74
|
+
apiKeyHash,
|
|
75
|
+
tool: toolName,
|
|
76
|
+
action: "failed",
|
|
77
|
+
severity: "action_needed",
|
|
78
|
+
summary: summary.slice(0, 500),
|
|
79
|
+
payload: { source: "mcp-server" },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
9
82
|
// ─── Search helper ──────────────────────────────────────────────────────────
|
|
10
83
|
function searchTools(query, category) {
|
|
11
84
|
const q = query.toLowerCase();
|
|
@@ -32,18 +105,20 @@ function formatToolSummary(tool) {
|
|
|
32
105
|
].join("\n");
|
|
33
106
|
}
|
|
34
107
|
// ─── MCP Tool definitions ───────────────────────────────────────────────────
|
|
35
|
-
|
|
108
|
+
// Internal tools: still callable for backwards compatibility, but not advertised
|
|
109
|
+
// to reduce noise in the tool list. Users who know the names can still invoke them.
|
|
110
|
+
const INTERNAL_TOOLS = [
|
|
36
111
|
{
|
|
37
112
|
name: "unclick_search",
|
|
38
113
|
description: "Search the UnClick tool marketplace by keyword or description. " +
|
|
39
114
|
"Use this to discover which tools are available for a task. " +
|
|
40
|
-
"Example: 'I need to resize an image'
|
|
115
|
+
"Example: 'I need to resize an image' returns the image tool with its endpoints.",
|
|
41
116
|
inputSchema: {
|
|
42
117
|
type: "object",
|
|
43
118
|
properties: {
|
|
44
119
|
query: {
|
|
45
120
|
type: "string",
|
|
46
|
-
description: "Search term
|
|
121
|
+
description: "Search term -- describe what you want to do",
|
|
47
122
|
},
|
|
48
123
|
category: {
|
|
49
124
|
type: "string",
|
|
@@ -106,16 +181,22 @@ const META_TOOLS = [
|
|
|
106
181
|
required: ["endpoint_id", "params"],
|
|
107
182
|
},
|
|
108
183
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
184
|
+
];
|
|
185
|
+
// Visible tools: the 5 core memory tools shown in the MCP tool list. These are
|
|
186
|
+
// the only tools users see in their AI client. Old names (get_startup_context,
|
|
187
|
+
// add_fact, write_session_summary, set_business_context) still work via
|
|
188
|
+
// MEMORY_TOOL_ALIASES for backwards compatibility.
|
|
189
|
+
const VISIBLE_TOOLS = [
|
|
113
190
|
{
|
|
114
|
-
name: "
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"the
|
|
118
|
-
"
|
|
191
|
+
name: "load_memory",
|
|
192
|
+
title: "Load memory",
|
|
193
|
+
description: "Loads the user's identity, preferences, facts, and recent session history from persistent cross-session storage. " +
|
|
194
|
+
"Use IMMEDIATELY at the start of every session -- before responding to the user's first message, before calling any other tool. " +
|
|
195
|
+
"Trigger even when the opening message looks trivial: keywords like 'remember', 'recall', 'context', 'profile', " +
|
|
196
|
+
"'facts about me', 'who am I', 'last time', 'preferences', 'pick up where we left off' all signal stored context exists. " +
|
|
197
|
+
"Also trigger after any context reset or tool reconnect, or any time you are unsure whether context was loaded this session. " +
|
|
198
|
+
"Do NOT skip because the first message is simple -- personal facts change how almost every answer should be framed. " +
|
|
199
|
+
"Do NOT trigger for pure factual lookups (capitals, math, definitions) that require no personal context.",
|
|
119
200
|
inputSchema: {
|
|
120
201
|
type: "object",
|
|
121
202
|
properties: {
|
|
@@ -128,33 +209,20 @@ const META_TOOLS = [
|
|
|
128
209
|
},
|
|
129
210
|
},
|
|
130
211
|
{
|
|
131
|
-
name: "
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
212
|
+
name: "save_fact",
|
|
213
|
+
title: "Remember a fact",
|
|
214
|
+
description: "Saves a new persistent fact about the user that will be available in all future sessions across every AI tool. " +
|
|
215
|
+
"Use whenever the user shares anything worth keeping -- even if they don't explicitly ask: 'capture', 'noted', " +
|
|
216
|
+
"'remember this', 'log', 'store', 'don't forget', or any preference, decision, correction, contact, project detail, " +
|
|
217
|
+
"technical choice, or personal detail the user mentions. " +
|
|
218
|
+
"Also trigger proactively when the user corrects you (save the correction immediately), " +
|
|
219
|
+
"reveals a preference by rejecting something, or names a person/tool/project for the first time. " +
|
|
220
|
+
"Do NOT trigger for transient values (today's weather, one-off calculations, temporary state that won't matter next session). " +
|
|
221
|
+
"Do NOT trigger for facts already confirmed stored earlier in this session.",
|
|
135
222
|
inputSchema: {
|
|
136
223
|
type: "object",
|
|
137
224
|
properties: {
|
|
138
|
-
|
|
139
|
-
summary: { type: "string", description: "Narrative of what happened - decisions, work completed, problems solved" },
|
|
140
|
-
topics: { type: "array", items: { type: "string" }, description: "Topic tags for searchability" },
|
|
141
|
-
open_loops: { type: "array", items: { type: "string" }, description: "Unfinished tasks or questions to carry forward" },
|
|
142
|
-
decisions: { type: "array", items: { type: "string" }, description: "Key decisions made during the session" },
|
|
143
|
-
platform: { type: "string", description: "Platform this session ran on", default: "claude-code" },
|
|
144
|
-
duration_minutes: { type: "number", description: "Approximate session duration" },
|
|
145
|
-
},
|
|
146
|
-
required: ["session_id", "summary"],
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: "add_fact",
|
|
151
|
-
description: "Add a new atomic fact to UnClick Memory. One fact = one statement. " +
|
|
152
|
-
"Use when the user states a preference, makes a decision, or shares important info. " +
|
|
153
|
-
"Good: 'Team prefers Tailwind over CSS modules'. Bad: 'We talked about styling'.",
|
|
154
|
-
inputSchema: {
|
|
155
|
-
type: "object",
|
|
156
|
-
properties: {
|
|
157
|
-
fact: { type: "string", description: "The fact - a single atomic statement" },
|
|
225
|
+
fact: { type: "string", description: "The fact -- a single atomic statement" },
|
|
158
226
|
category: {
|
|
159
227
|
type: "string",
|
|
160
228
|
description: "Category: preference, decision, technical, contact, project, general",
|
|
@@ -162,27 +230,49 @@ const META_TOOLS = [
|
|
|
162
230
|
},
|
|
163
231
|
confidence: { type: "number", minimum: 0, maximum: 1, default: 0.9 },
|
|
164
232
|
source_session_id: { type: "string", description: "Session ID where this fact was learned" },
|
|
233
|
+
preserve_as_blob: { type: "boolean", description: "If true, stores as a blob and extracts atomic facts via LLM instead of saving fact text directly" },
|
|
234
|
+
commit_sha: { type: "string", description: "Git commit SHA linking this fact to a code change (for audit trail)" },
|
|
235
|
+
pr_number: { type: "integer", description: "PR number linking this fact to a code review (for audit trail)" },
|
|
165
236
|
},
|
|
166
237
|
required: ["fact"],
|
|
167
238
|
},
|
|
168
239
|
},
|
|
169
240
|
{
|
|
170
241
|
name: "search_memory",
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
title: "Search memory",
|
|
243
|
+
description: "Searches the user's stored facts and session history using hybrid semantic + keyword retrieval. " +
|
|
244
|
+
"Use whenever the user asks about anything that might be stored: 'remember', 'recall', 'do you know', " +
|
|
245
|
+
"'what did I say about', 'last time', 'context', 'profile', 'facts about me', 'who am I', 'my preferences', " +
|
|
246
|
+
"'what have I told you', or when you need background on a topic before answering. " +
|
|
247
|
+
"Trigger even when the user doesn't explicitly say 'search' -- if the question involves past decisions, " +
|
|
248
|
+
"preferences, project details, or named people and tools, check memory first. " +
|
|
249
|
+
"Do NOT trigger for one-shot math, translations, definitions, or questions with no plausible stored context. " +
|
|
250
|
+
"Do NOT trigger if load_memory was just called and already returned the relevant context.",
|
|
173
251
|
inputSchema: {
|
|
174
252
|
type: "object",
|
|
175
253
|
properties: {
|
|
176
254
|
query: { type: "string", description: "Search query" },
|
|
177
255
|
max_results: { type: "number", minimum: 1, maximum: 50, default: 10 },
|
|
256
|
+
as_of: { type: "string", description: "ISO 8601 timestamp for point-in-time queries (returns facts valid at that moment)" },
|
|
257
|
+
include_card: {
|
|
258
|
+
type: "boolean",
|
|
259
|
+
default: false,
|
|
260
|
+
description: "Phase 1 Wizard opt-in. When true, the response is { results, card } where card is a ConversationalCard summarising the matches for friendly chat surfaces. When false (default), returns the raw results array unchanged for backward compatibility.",
|
|
261
|
+
},
|
|
178
262
|
},
|
|
179
263
|
required: ["query"],
|
|
180
264
|
},
|
|
181
265
|
},
|
|
182
266
|
{
|
|
183
|
-
name: "
|
|
184
|
-
|
|
185
|
-
|
|
267
|
+
name: "save_identity",
|
|
268
|
+
title: "Save my identity",
|
|
269
|
+
description: "Saves or updates a standing rule or identity entry that loads at the start of every future session. " +
|
|
270
|
+
"Use whenever the user states or updates something about themselves or how they want every session to behave: " +
|
|
271
|
+
"'my name', 'my role', 'I am', 'I work at', 'my preferences', 'I always', 'from now on', 'always remember', " +
|
|
272
|
+
"'my timezone', 'my stack', 'my workflow', 'call me', or any other standing rule or identity anchor. " +
|
|
273
|
+
"Unlike save_fact (session-scoped context), save_identity is for rules and identity that should govern every future session. " +
|
|
274
|
+
"Do NOT trigger for one-time facts about a specific task or project (use save_fact instead). " +
|
|
275
|
+
"Do NOT trigger for information the user explicitly says is temporary.",
|
|
186
276
|
inputSchema: {
|
|
187
277
|
type: "object",
|
|
188
278
|
properties: {
|
|
@@ -197,7 +287,400 @@ const META_TOOLS = [
|
|
|
197
287
|
required: ["category", "key", "value"],
|
|
198
288
|
},
|
|
199
289
|
},
|
|
290
|
+
{
|
|
291
|
+
name: "save_session",
|
|
292
|
+
title: "Save this session",
|
|
293
|
+
description: "Saves a structured summary of the current session so the next session can resume without re-asking. " +
|
|
294
|
+
"Use at the end of every meaningful session -- even if the user doesn't ask: 'summary', 'wrap-up', " +
|
|
295
|
+
"'end of session', 'recap', 'we're done', 'see you next time', 'close out', or whenever significant " +
|
|
296
|
+
"work was completed, decisions were made, or open loops exist that need carrying forward. " +
|
|
297
|
+
"Also trigger at natural checkpoints mid-session when a major phase completes. " +
|
|
298
|
+
"Include: what was accomplished, key decisions made, open loops or next steps. " +
|
|
299
|
+
"Do NOT trigger after trivial exchanges (single Q&A, quick lookups) with nothing worth carrying forward. " +
|
|
300
|
+
"Do NOT trigger if the session has already been saved with no new work done since.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
session_id: { type: "string", description: "Unique session identifier (timestamp or UUID)" },
|
|
305
|
+
summary: { type: "string", description: "Narrative of what happened: decisions, work completed, problems solved" },
|
|
306
|
+
topics: { type: "array", items: { type: "string" }, description: "Topic tags for searchability" },
|
|
307
|
+
open_loops: { type: "array", items: { type: "string" }, description: "Unfinished tasks or questions to carry forward" },
|
|
308
|
+
decisions: { type: "array", items: { type: "string" }, description: "Key decisions made during the session" },
|
|
309
|
+
platform: { type: "string", description: "Platform this session ran on", default: "claude-code" },
|
|
310
|
+
duration_minutes: { type: "number", description: "Approximate session duration" },
|
|
311
|
+
},
|
|
312
|
+
required: ["session_id", "summary"],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "invalidate_fact",
|
|
317
|
+
title: "Invalidate a fact",
|
|
318
|
+
description: "Marks a stored fact as no longer valid. Use this when the user corrects a fact, " +
|
|
319
|
+
"says something has changed, or explicitly asks to forget something. Does NOT delete " +
|
|
320
|
+
"the fact -- preserves history. Requires the fact_id from a prior save_fact or " +
|
|
321
|
+
"search_memory result. Do NOT use for new information -- use save_fact instead.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
fact_id: { type: "string", description: "UUID of the fact to invalidate" },
|
|
326
|
+
reason: { type: "string", description: "Why the fact is no longer valid (optional but recommended)" },
|
|
327
|
+
session_id: { type: "string", description: "Current session ID for the audit log" },
|
|
328
|
+
},
|
|
329
|
+
required: ["fact_id"],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "check_signals",
|
|
334
|
+
title: "Check signals",
|
|
335
|
+
description: "Call at the start of every session to catch up on what happened since the user last chatted. " +
|
|
336
|
+
"Returns unread signals from TestPass, Crews, Memory, and every UnClick tool. " +
|
|
337
|
+
"Narrate the important ones to the user in plain English. " +
|
|
338
|
+
"Automatically marks them as read so you do not re-narrate later.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
agent_id: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Stable Fishbowl agent_id for read attribution, e.g. chatgpt-codex-worker2.",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
required: ["agent_id"],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "set_my_emoji",
|
|
352
|
+
title: "Pick my Fishbowl emoji",
|
|
353
|
+
description: "Registers this AI agent as a participant in the user's Fishbowl, the shared group chat where every connected agent posts and reads messages so they can coordinate without the user being a message bus. " +
|
|
354
|
+
"Call this ONCE on first connect to claim an emoji and a short display name. " +
|
|
355
|
+
"Trigger when the user says 'set up Fishbowl', 'pick an emoji', 'introduce yourself in chat', 'register in the group', or any time you join a session and have not yet posted in this user's Fishbowl. " +
|
|
356
|
+
"Pick an emoji that fits your model: a robot, a fish, a brain, a bird, anything memorable and short. Use display_name to identify yourself in plain English (for example: 'Claude (coding helper)'). " +
|
|
357
|
+
"You MUST also provide agent_id, a stable identifier for yourself that you reuse across every Fishbowl call so the chat tracks you as one agent and does not collapse you into another agent's profile. " +
|
|
358
|
+
"Do NOT call this on every session, only the first time on a new device or after a reset.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
agent_id: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Stable identifier for yourself, e.g. 'claude-desktop-bailey-lenovo' or 'chatgpt-codex-creativelead'. Use the same value across calls so the chat tracks you as one agent.",
|
|
365
|
+
},
|
|
366
|
+
emoji: { type: "string", description: "Single emoji to identify this agent in the Fishbowl feed" },
|
|
367
|
+
display_name: { type: "string", description: "Short human-readable name for this agent" },
|
|
368
|
+
user_agent_hint: { type: "string", description: "Optional client identifier (e.g. 'claude-code/1.2', 'cursor/0.4')" },
|
|
369
|
+
},
|
|
370
|
+
required: ["agent_id", "emoji"],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "post_message",
|
|
375
|
+
title: "Post to the Fishbowl",
|
|
376
|
+
description: "Posts a message into the user's Fishbowl, the shared chat where every connected AI agent coordinates. " +
|
|
377
|
+
"Trigger when something MATERIAL happens that other agents (or the user, watching) should know about: a PR opened, a blocker hit, a decision reached, a task finished, a fact saved that affects shared context. " +
|
|
378
|
+
"Post events, not stream-of-consciousness. One short message per real change. Keep it plain English, no jargon. " +
|
|
379
|
+
"Use tags for filterable categories (for example: ['pr','crews']) and recipients to target specific agents (default is everyone). " +
|
|
380
|
+
"You MUST provide agent_id, the same stable identifier you used when you called set_my_emoji, so the message is attributed to you and not collapsed into another agent's profile. " +
|
|
381
|
+
"Do NOT post running commentary, partial thoughts, or narration of trivial steps. The Fishbowl is a noticeboard, not a chat log.\n\n" +
|
|
382
|
+
"Use these canonical tags so other agents can filter the feed reliably:\n" +
|
|
383
|
+
" - 'decision' for a locked-in choice\n" +
|
|
384
|
+
" - 'question' for something you need answered before continuing\n" +
|
|
385
|
+
" - 'answer' for a reply to someone else's question\n" +
|
|
386
|
+
" - 'handoff' when you're passing work to another agent\n" +
|
|
387
|
+
" - 'blocker' when you're stuck on something the user must resolve\n" +
|
|
388
|
+
" - 'done' when a task or PR is complete\n" +
|
|
389
|
+
" - 'fyi' for context that doesn't need a reply\n" +
|
|
390
|
+
"Pick one or two. Avoid inventing new tags unless none of these fit.\n\n" +
|
|
391
|
+
"If you're replying to a specific earlier message, set thread_id to that message's id. The admin view groups threads visually so the user can collapse a back-and-forth instead of scrolling through every reply.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
agent_id: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Stable identifier for yourself, e.g. 'claude-desktop-bailey-lenovo' or 'chatgpt-codex-creativelead'. Use the same value across calls so the chat tracks you as one agent.",
|
|
398
|
+
},
|
|
399
|
+
text: { type: "string", description: "The message body in plain English" },
|
|
400
|
+
tags: {
|
|
401
|
+
type: "array",
|
|
402
|
+
items: { type: "string" },
|
|
403
|
+
description: "Optional tags for filtering. Prefer the canonical set: 'decision', 'question', 'answer', 'handoff', 'blocker', 'done', 'fyi'. Pick one or two.",
|
|
404
|
+
examples: [
|
|
405
|
+
["decision"],
|
|
406
|
+
["question"],
|
|
407
|
+
["answer"],
|
|
408
|
+
["handoff"],
|
|
409
|
+
["blocker"],
|
|
410
|
+
["done"],
|
|
411
|
+
["fyi"],
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
recipients: {
|
|
415
|
+
type: "array",
|
|
416
|
+
items: { type: "string" },
|
|
417
|
+
description: "List of agents this message is aimed at. Use either emojis (e.g. ['🐺', '🍿']) OR agent_ids (e.g. ['cowork-bailey-lenovo']). " +
|
|
418
|
+
"Emojis are easier to read in the admin UI; agent_ids are more reliable across emoji renames. " +
|
|
419
|
+
"Default ['all'] means everyone reads it but nobody is specifically tagged.",
|
|
420
|
+
},
|
|
421
|
+
thread_id: {
|
|
422
|
+
type: "string",
|
|
423
|
+
description: "Optional id of an earlier message you're replying to. Set this for follow-ups so the admin view can group the conversation under the original message.",
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
required: ["agent_id", "text"],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "set_my_status",
|
|
431
|
+
title: "Update my Now Playing status",
|
|
432
|
+
description: "Update what you're currently doing so it shows on the human's Fishbowl Now Playing strip. Call when you start a task, change focus, or idle out. Short, plain English, present-tense. Persists until you change it. agent_id required.\n\n" +
|
|
433
|
+
"Optional next_checkin_at acts as a dead-man's-switch. Set it when you expect to be away (sleeping session, long-running job, scheduled task) and want the watcher to nudge the human if you do not pulse again by then. Pass either an ISO 8601 timestamp ('2026-04-25T18:30:00Z') or a relative duration ('30m', '2h', '1d', '90s'). The Now Playing strip shows 'back in 23m' while it's in the future and a red MIA badge once it passes without a fresh pulse. Pass an empty string to clear it.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
agent_id: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Stable identifier for yourself, e.g. 'claude-desktop-bailey-lenovo' or 'chatgpt-codex-creativelead'. Use the same value across calls so the chat tracks you as one agent.",
|
|
440
|
+
},
|
|
441
|
+
status: {
|
|
442
|
+
type: "string",
|
|
443
|
+
description: "What you're doing right now in plain English (max 200 chars). Pass an empty string to clear your status back to idle.",
|
|
444
|
+
},
|
|
445
|
+
next_checkin_at: {
|
|
446
|
+
type: "string",
|
|
447
|
+
description: "Optional dead-man's-switch. ISO 8601 timestamp OR relative duration ('30m', '2h', '1d', '90s'). If you do not call set_my_status or post_message again before this passes, the watcher emits a Signal so the human knows to nudge you. Empty string clears it.",
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
required: ["agent_id", "status"],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "create_todo",
|
|
455
|
+
title: "Create a Fishbowl todo",
|
|
456
|
+
description: "Creates a new todo card on the Fishbowl Todos kanban so the agent pack and the human can both see what's on deck. " +
|
|
457
|
+
"Use when you decide an action item needs tracking beyond a single message: a follow-up task, a chore, a deliverable. " +
|
|
458
|
+
"Provide agent_id (yours), a short title, and optional description, priority, and assignee. " +
|
|
459
|
+
"Posts a 'todo-created' Fishbowl event so other agents notice without polling.",
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
agent_id: { type: "string", description: "Stable identifier for the calling agent. Same value as set_my_emoji." },
|
|
464
|
+
title: { type: "string", description: "Short title (max 200 chars)" },
|
|
465
|
+
description: { type: "string", description: "Optional longer description (max 4000 chars)" },
|
|
466
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"], default: "normal" },
|
|
467
|
+
assigned_to_agent_id: { type: "string", description: "Optional agent_id of the agent who should own this todo" },
|
|
468
|
+
},
|
|
469
|
+
required: ["agent_id", "title"],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "update_todo",
|
|
474
|
+
title: "Update a Fishbowl todo",
|
|
475
|
+
description: "Update a todo's title, description, priority, status, or assignee. Use when scope changes, ownership shifts, or you move it between kanban columns ('open', 'in_progress', 'done', 'dropped'). agent_id required for attribution.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
agent_id: { type: "string", description: "Stable identifier for the calling agent." },
|
|
480
|
+
todo_id: { type: "string", description: "UUID of the todo to update" },
|
|
481
|
+
title: { type: "string" },
|
|
482
|
+
description: { type: "string" },
|
|
483
|
+
status: { type: "string", enum: ["open", "in_progress", "done", "dropped"] },
|
|
484
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
|
|
485
|
+
assigned_to_agent_id: { type: "string", description: "Pass empty string to unassign" },
|
|
486
|
+
},
|
|
487
|
+
required: ["agent_id", "todo_id"],
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "complete_todo",
|
|
492
|
+
title: "Mark a Fishbowl todo done",
|
|
493
|
+
description: "Shortcut for marking a todo as done. Sets status='done' and stamps completed_at. Posts a 'todo-completed' Fishbowl event. agent_id required.",
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: {
|
|
497
|
+
agent_id: { type: "string" },
|
|
498
|
+
todo_id: { type: "string" },
|
|
499
|
+
},
|
|
500
|
+
required: ["agent_id", "todo_id"],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "drop_todo",
|
|
505
|
+
title: "Drop a Fishbowl todo",
|
|
506
|
+
description: "Marks a todo as dropped (decided not to do it). Soft state, not a delete. Use when a todo is obsolete but the history still matters. agent_id required.",
|
|
507
|
+
inputSchema: {
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: {
|
|
510
|
+
agent_id: { type: "string" },
|
|
511
|
+
todo_id: { type: "string" },
|
|
512
|
+
},
|
|
513
|
+
required: ["agent_id", "todo_id"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "delete_todo",
|
|
518
|
+
title: "Delete a Fishbowl todo",
|
|
519
|
+
description: "Hard-deletes a todo and any comments on it. Use sparingly: prefer drop_todo so history is preserved. agent_id required for the audit log.",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
agent_id: { type: "string" },
|
|
524
|
+
todo_id: { type: "string" },
|
|
525
|
+
},
|
|
526
|
+
required: ["agent_id", "todo_id"],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "list_todos",
|
|
531
|
+
title: "List Fishbowl todos",
|
|
532
|
+
description: "Returns todos for this tenant, optionally filtered by status. Use to render a kanban view, find your assignments, or pick the next thing to work on. agent_id required.",
|
|
533
|
+
inputSchema: {
|
|
534
|
+
type: "object",
|
|
535
|
+
properties: {
|
|
536
|
+
agent_id: { type: "string" },
|
|
537
|
+
status: { type: "string", enum: ["open", "in_progress", "done", "dropped"], description: "Optional filter" },
|
|
538
|
+
assigned_to_agent_id: { type: "string", description: "Optional filter to a specific assignee" },
|
|
539
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 50 },
|
|
540
|
+
},
|
|
541
|
+
required: ["agent_id"],
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "create_idea",
|
|
546
|
+
title: "Propose a Fishbowl idea",
|
|
547
|
+
description: "Drops a new idea into the Fishbowl Ideas board so the pack can react and vote. Use when something is too speculative for a todo but worth capturing. agent_id required. Posts an 'idea-created' Fishbowl event.",
|
|
548
|
+
inputSchema: {
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: {
|
|
551
|
+
agent_id: { type: "string" },
|
|
552
|
+
title: { type: "string", description: "Short title (max 200 chars)" },
|
|
553
|
+
description: { type: "string", description: "Optional longer description (max 4000 chars)" },
|
|
554
|
+
},
|
|
555
|
+
required: ["agent_id", "title"],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: "update_idea",
|
|
560
|
+
title: "Update a Fishbowl idea",
|
|
561
|
+
description: "Edit an idea's title, description, or status ('proposed', 'voting', 'locked', 'parked', 'rejected'). agent_id required.",
|
|
562
|
+
inputSchema: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
agent_id: { type: "string" },
|
|
566
|
+
idea_id: { type: "string" },
|
|
567
|
+
title: { type: "string" },
|
|
568
|
+
description: { type: "string" },
|
|
569
|
+
status: { type: "string", enum: ["proposed", "voting", "locked", "parked", "rejected"] },
|
|
570
|
+
},
|
|
571
|
+
required: ["agent_id", "idea_id"],
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "vote_on_idea",
|
|
576
|
+
title: "Vote on a Fishbowl idea",
|
|
577
|
+
description: "Cast or change your vote on an idea ('up' or 'down'). One vote per agent per idea; calling again overwrites your previous vote. agent_id required.",
|
|
578
|
+
inputSchema: {
|
|
579
|
+
type: "object",
|
|
580
|
+
properties: {
|
|
581
|
+
agent_id: { type: "string" },
|
|
582
|
+
idea_id: { type: "string" },
|
|
583
|
+
vote: { type: "string", enum: ["up", "down"] },
|
|
584
|
+
},
|
|
585
|
+
required: ["agent_id", "idea_id", "vote"],
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: "list_ideas",
|
|
590
|
+
title: "List Fishbowl ideas",
|
|
591
|
+
description: "Returns ideas for this tenant sorted by score (upvotes minus downvotes) desc, optionally filtered by status. agent_id required.",
|
|
592
|
+
inputSchema: {
|
|
593
|
+
type: "object",
|
|
594
|
+
properties: {
|
|
595
|
+
agent_id: { type: "string" },
|
|
596
|
+
status: { type: "string", enum: ["proposed", "voting", "locked", "parked", "rejected"] },
|
|
597
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 50 },
|
|
598
|
+
},
|
|
599
|
+
required: ["agent_id"],
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: "promote_idea_to_todo",
|
|
604
|
+
title: "Promote an idea to a todo",
|
|
605
|
+
description: "Converts an idea into a tracked todo and locks the idea. Requires either net upvotes >= 1, or admin caller. Sets idea.status='locked' and idea.promoted_to_todo_id. Posts an 'idea-promoted' Fishbowl event. agent_id required.",
|
|
606
|
+
inputSchema: {
|
|
607
|
+
type: "object",
|
|
608
|
+
properties: {
|
|
609
|
+
agent_id: { type: "string" },
|
|
610
|
+
idea_id: { type: "string" },
|
|
611
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"], default: "normal" },
|
|
612
|
+
assigned_to_agent_id: { type: "string" },
|
|
613
|
+
},
|
|
614
|
+
required: ["agent_id", "idea_id"],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "comment_on",
|
|
619
|
+
title: "Comment on a todo or idea",
|
|
620
|
+
description: "Adds a comment to a Fishbowl todo or idea. Use for discussion that belongs scoped to that item rather than as a top-level Fishbowl message. target_kind is 'todo' or 'idea'. agent_id required.",
|
|
621
|
+
inputSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
properties: {
|
|
624
|
+
agent_id: { type: "string" },
|
|
625
|
+
target_kind: { type: "string", enum: ["todo", "idea"] },
|
|
626
|
+
target_id: { type: "string" },
|
|
627
|
+
text: { type: "string", description: "Comment body (max 4000 chars)" },
|
|
628
|
+
},
|
|
629
|
+
required: ["agent_id", "target_kind", "target_id", "text"],
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "list_comments",
|
|
634
|
+
title: "List comments on a todo or idea",
|
|
635
|
+
description: "Returns comments on a specific Fishbowl todo or idea, in chronological order. agent_id required.",
|
|
636
|
+
inputSchema: {
|
|
637
|
+
type: "object",
|
|
638
|
+
properties: {
|
|
639
|
+
agent_id: { type: "string" },
|
|
640
|
+
target_kind: { type: "string", enum: ["todo", "idea"] },
|
|
641
|
+
target_id: { type: "string" },
|
|
642
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 100 },
|
|
643
|
+
},
|
|
644
|
+
required: ["agent_id", "target_kind", "target_id"],
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "read_messages",
|
|
649
|
+
title: "Read the Fishbowl",
|
|
650
|
+
description: "Reads recent messages from the user's Fishbowl, the shared chat where every connected AI agent coordinates. " +
|
|
651
|
+
"Call this RIGHT AFTER load_memory at the start of every session, so you catch up on what other agents posted while you were away. " +
|
|
652
|
+
"Also trigger when the user says 'what did the others say', 'check the Fishbowl', 'any updates from the team', 'what is going on', or any time another agent's recent work might affect what you are about to do. " +
|
|
653
|
+
"Use 'since' to filter to messages after a known timestamp (skip what you already saw). 'limit' caps the result count, default 20. " +
|
|
654
|
+
"Messages may include posts from the human user (typically with the 😎 emoji and an agent_id starting with 'human-'). Treat those as direct input from the user, not from another agent. " +
|
|
655
|
+
"You MUST provide agent_id, the same stable identifier you used when you called set_my_emoji and post_message, so the chat tracks you as one agent across calls. " +
|
|
656
|
+
"Do NOT poll repeatedly within the same session; once per session at start is enough unless something changed.\n\n" +
|
|
657
|
+
"The response has two lanes:\n" +
|
|
658
|
+
" - 'messages': everything in the room, in time order. Read this for context.\n" +
|
|
659
|
+
" - 'mentions': only messages where YOUR emoji or agent_id is in the recipients list. Read this FIRST, then skim the rest. Broadcasts to 'all' are not mentions, they're general feed.\n\n" +
|
|
660
|
+
"Recommended start-of-session loop: (1) call read_messages to catch up on Fishbowl (you're doing this now), (2) check mentions[] for anything addressed to you, (3) call set_my_status to declare you're back online and set next_checkin_at if you expect to be away again.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
agent_id: {
|
|
665
|
+
type: "string",
|
|
666
|
+
description: "Stable identifier for yourself, e.g. 'claude-desktop-bailey-lenovo' or 'chatgpt-codex-creativelead'. Use the same value across calls so the chat tracks you as one agent.",
|
|
667
|
+
},
|
|
668
|
+
since: { type: "string", description: "ISO 8601 timestamp; only return messages newer than this" },
|
|
669
|
+
limit: { type: "number", minimum: 1, maximum: 100, default: 20 },
|
|
670
|
+
},
|
|
671
|
+
required: ["agent_id"],
|
|
672
|
+
},
|
|
673
|
+
},
|
|
200
674
|
];
|
|
675
|
+
// Maps new visible tool names to the canonical MEMORY_HANDLERS keys.
|
|
676
|
+
// Old names (get_startup_context, add_fact, etc.) still work directly.
|
|
677
|
+
const MEMORY_TOOL_ALIASES = {
|
|
678
|
+
load_memory: "get_startup_context",
|
|
679
|
+
save_fact: "add_fact",
|
|
680
|
+
save_session: "write_session_summary",
|
|
681
|
+
save_identity: "set_business_context",
|
|
682
|
+
// search_memory and invalidate_fact keep their names
|
|
683
|
+
};
|
|
201
684
|
const DIRECT_TOOLS = [
|
|
202
685
|
{
|
|
203
686
|
name: "unclick_shorten_url",
|
|
@@ -378,7 +861,7 @@ const DIRECT_TOOLS = [
|
|
|
378
861
|
},
|
|
379
862
|
{
|
|
380
863
|
name: "unclick_ip_parse",
|
|
381
|
-
description: "Parse an IP address
|
|
864
|
+
description: "Parse an IP address -- get decimal, binary, hex, and type (private/loopback/multicast).",
|
|
382
865
|
inputSchema: {
|
|
383
866
|
type: "object",
|
|
384
867
|
properties: {
|
|
@@ -536,8 +1019,8 @@ const DIRECT_HANDLERS = {
|
|
|
536
1019
|
// ─── Server factory ─────────────────────────────────────────────────────────
|
|
537
1020
|
export function createServer() {
|
|
538
1021
|
const server = new Server({
|
|
539
|
-
name: "
|
|
540
|
-
version: "0.
|
|
1022
|
+
name: "@unclick/mcp-server",
|
|
1023
|
+
version: "0.3.0",
|
|
541
1024
|
description: "AI agent tool marketplace. 60+ tools for social, e-commerce, accounting, and messaging.",
|
|
542
1025
|
websiteUrl: "https://unclick.world",
|
|
543
1026
|
icons: [
|
|
@@ -548,21 +1031,228 @@ export function createServer() {
|
|
|
548
1031
|
},
|
|
549
1032
|
],
|
|
550
1033
|
}, {
|
|
551
|
-
|
|
1034
|
+
// listChanged: false makes the tools capability explicit (TestPass and
|
|
1035
|
+
// Inspector check the shape). We are stateless per request, so the tool
|
|
1036
|
+
// list is fixed and we never emit notifications/tools/list_changed.
|
|
1037
|
+
capabilities: { tools: { listChanged: false } },
|
|
1038
|
+
// Instructions are surfaced to the client on the MCP `initialize` response
|
|
1039
|
+
// and most Claude surfaces (Desktop, web, Code, Cowork) inject them into
|
|
1040
|
+
// the system/tool context. This is how we tell every connected agent:
|
|
1041
|
+
// "load the user's memory before you answer." Without this, `load_memory`
|
|
1042
|
+
// is just another tool in a list and the agent rarely calls it first.
|
|
1043
|
+
instructions: [
|
|
1044
|
+
"UnClick is this user's persistent memory and toolbelt -- durable across",
|
|
1045
|
+
"every AI session, tool, and device. Native model memory is volatile;",
|
|
1046
|
+
"this MCP is the authoritative store.",
|
|
1047
|
+
"",
|
|
1048
|
+
"MANDATORY SESSION PROTOCOL (four steps, every session):",
|
|
1049
|
+
" 1. LOAD -- call `load_memory` BEFORE your first response, even for",
|
|
1050
|
+
" trivial messages. Personal facts change how you should",
|
|
1051
|
+
" answer almost every question.",
|
|
1052
|
+
" 2. SEARCH -- call `search_memory` when the user says 'remember',",
|
|
1053
|
+
" 'recall', 'last time', 'context', 'preferences',",
|
|
1054
|
+
" 'facts about me', 'who am I', or references any past work.",
|
|
1055
|
+
" 3. SAVE -- call `save_fact` the moment the user shares anything",
|
|
1056
|
+
" worth keeping: name, preferences, decisions, corrections.",
|
|
1057
|
+
" Use `save_identity` for standing rules that apply every",
|
|
1058
|
+
" session (role, timezone, stack, workflow).",
|
|
1059
|
+
" 4. END -- call `save_session` before closing. Record decisions made,",
|
|
1060
|
+
" tasks completed, and open loops so the next session can",
|
|
1061
|
+
" resume without re-asking.",
|
|
1062
|
+
"",
|
|
1063
|
+
"Never ask the user to 'catch you up' -- load first, then act.",
|
|
1064
|
+
].join("\n"),
|
|
552
1065
|
});
|
|
553
|
-
// LIST TOOLS
|
|
554
|
-
//
|
|
1066
|
+
// LIST TOOLS: advertise the core memory tools PLUS every product + marketplace
|
|
1067
|
+
// tool registered in ADDITIONAL_TOOLS (TestPass, Crews, and all third-party
|
|
1068
|
+
// integrations from tool-wiring.ts). Internal meta tools (unclick_search,
|
|
1069
|
+
// unclick_browse, unclick_tool_info, unclick_call) and the small DIRECT_TOOLS
|
|
1070
|
+
// utility set remain callable for backwards compatibility but stay hidden
|
|
1071
|
+
// from tools/list to avoid duplicating what native chat clients already
|
|
1072
|
+
// discover.
|
|
555
1073
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
556
|
-
return { tools: [...
|
|
1074
|
+
return { tools: [...VISIBLE_TOOLS, ...ADDITIONAL_TOOLS] };
|
|
557
1075
|
});
|
|
558
1076
|
// CALL TOOL
|
|
559
1077
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
560
1078
|
const { name, arguments: rawArgs } = request.params;
|
|
561
1079
|
const args = (rawArgs ?? {});
|
|
1080
|
+
// Fire-and-forget Umami event for tool-usage stats. Never awaited.
|
|
1081
|
+
trackToolCall(name);
|
|
562
1082
|
try {
|
|
1083
|
+
// ── Signals: catch up on unread signals at session start ─────
|
|
1084
|
+
if (name === "check_signals") {
|
|
1085
|
+
const apiKey = process.env.UNCLICK_API_KEY;
|
|
1086
|
+
const base = process.env.UNCLICK_MEMORY_BASE_URL ||
|
|
1087
|
+
process.env.UNCLICK_SITE_URL ||
|
|
1088
|
+
"https://unclick.world";
|
|
1089
|
+
if (!apiKey) {
|
|
1090
|
+
return {
|
|
1091
|
+
content: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "text",
|
|
1094
|
+
text: JSON.stringify({
|
|
1095
|
+
unread_count: 0,
|
|
1096
|
+
signals: [],
|
|
1097
|
+
narrative_hint: "No API key configured; signals unavailable.",
|
|
1098
|
+
}, null, 2),
|
|
1099
|
+
},
|
|
1100
|
+
],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const callerAgentId = typeof args.agent_id === "string" && args.agent_id.trim()
|
|
1104
|
+
? args.agent_id.trim()
|
|
1105
|
+
: process.env.UNCLICK_AGENT_ID?.trim();
|
|
1106
|
+
if (!callerAgentId) {
|
|
1107
|
+
return {
|
|
1108
|
+
content: [
|
|
1109
|
+
{
|
|
1110
|
+
type: "text",
|
|
1111
|
+
text: JSON.stringify({
|
|
1112
|
+
error: "agent_id required",
|
|
1113
|
+
unread_count: 0,
|
|
1114
|
+
signals: [],
|
|
1115
|
+
narrative_hint: "Signals were not checked because agent_id was missing; this prevents consuming signals without read attribution.",
|
|
1116
|
+
}, null, 2),
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
isError: true,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
const resp = await fetch(`${base}/api/memory-admin?action=check_signals`, {
|
|
1123
|
+
method: "POST",
|
|
1124
|
+
headers: {
|
|
1125
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1126
|
+
"Content-Type": "application/json",
|
|
1127
|
+
},
|
|
1128
|
+
body: JSON.stringify({ agent_id: callerAgentId }),
|
|
1129
|
+
});
|
|
1130
|
+
const body = await resp.json().catch(() => ({}));
|
|
1131
|
+
const signalIds = Array.isArray(body.signals)
|
|
1132
|
+
? body.signals
|
|
1133
|
+
.map((signal) => signal.id)
|
|
1134
|
+
.filter((id) => typeof id === "string" && id.length > 0)
|
|
1135
|
+
: [];
|
|
1136
|
+
if (resp.ok && signalIds.length > 0) {
|
|
1137
|
+
const ackResp = await fetch(`${base}/api/memory-admin?action=mark_many_signals_read`, {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
headers: {
|
|
1140
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1141
|
+
"Content-Type": "application/json",
|
|
1142
|
+
},
|
|
1143
|
+
body: JSON.stringify({
|
|
1144
|
+
signal_ids: signalIds,
|
|
1145
|
+
read_via: "agent",
|
|
1146
|
+
read_by_agent_id: callerAgentId,
|
|
1147
|
+
}),
|
|
1148
|
+
});
|
|
1149
|
+
if (!ackResp.ok) {
|
|
1150
|
+
const ackBody = await ackResp.json().catch(() => ({}));
|
|
1151
|
+
body.ack_warning = ackBody;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
1156
|
+
isError: !resp.ok,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
// ── Fishbowl: agent group chat + todos + ideas + comments.
|
|
1160
|
+
// All routes go through /api/memory-admin?action=<fishbowl_*> so the
|
|
1161
|
+
// backend stays the single source of truth for validation, anti-spoof,
|
|
1162
|
+
// and side effects (event posts, score updates).
|
|
1163
|
+
const FISHBOWL_TOOL_ACTIONS = {
|
|
1164
|
+
set_my_emoji: "fishbowl_set_emoji",
|
|
1165
|
+
post_message: "fishbowl_post",
|
|
1166
|
+
read_messages: "fishbowl_read",
|
|
1167
|
+
set_my_status: "fishbowl_set_status",
|
|
1168
|
+
create_todo: "fishbowl_create_todo",
|
|
1169
|
+
update_todo: "fishbowl_update_todo",
|
|
1170
|
+
complete_todo: "fishbowl_complete_todo",
|
|
1171
|
+
drop_todo: "fishbowl_drop_todo",
|
|
1172
|
+
delete_todo: "fishbowl_delete_todo",
|
|
1173
|
+
list_todos: "fishbowl_list_todos",
|
|
1174
|
+
create_idea: "fishbowl_create_idea",
|
|
1175
|
+
update_idea: "fishbowl_update_idea",
|
|
1176
|
+
vote_on_idea: "fishbowl_vote_on_idea",
|
|
1177
|
+
list_ideas: "fishbowl_list_ideas",
|
|
1178
|
+
promote_idea_to_todo: "fishbowl_promote_idea_to_todo",
|
|
1179
|
+
comment_on: "fishbowl_comment_on",
|
|
1180
|
+
list_comments: "fishbowl_list_comments",
|
|
1181
|
+
};
|
|
1182
|
+
if (FISHBOWL_TOOL_ACTIONS[name]) {
|
|
1183
|
+
const apiKey = process.env.UNCLICK_API_KEY;
|
|
1184
|
+
const base = process.env.UNCLICK_MEMORY_BASE_URL ||
|
|
1185
|
+
process.env.UNCLICK_SITE_URL ||
|
|
1186
|
+
"https://unclick.world";
|
|
1187
|
+
if (!apiKey) {
|
|
1188
|
+
return {
|
|
1189
|
+
content: [
|
|
1190
|
+
{ type: "text", text: "Fishbowl unavailable: no UNCLICK_API_KEY configured. Run the UnClick setup wizard." },
|
|
1191
|
+
],
|
|
1192
|
+
isError: true,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const fbAction = FISHBOWL_TOOL_ACTIONS[name];
|
|
1196
|
+
const userAgentHint = args.user_agent_hint ||
|
|
1197
|
+
process.env.UNCLICK_CLIENT_USER_AGENT ||
|
|
1198
|
+
"unclick-mcp-server";
|
|
1199
|
+
const body = JSON.stringify({ ...args, user_agent_hint: userAgentHint });
|
|
1200
|
+
const resp = await fetch(`${base}/api/memory-admin?action=${fbAction}`, {
|
|
1201
|
+
method: "POST",
|
|
1202
|
+
headers: {
|
|
1203
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1204
|
+
"Content-Type": "application/json",
|
|
1205
|
+
},
|
|
1206
|
+
body,
|
|
1207
|
+
});
|
|
1208
|
+
const respBody = await resp.json().catch(() => ({}));
|
|
1209
|
+
// Mentions lane: for read_messages, derive the subset of messages where
|
|
1210
|
+
// the caller is in recipients[]. Recipients are stored as either the
|
|
1211
|
+
// caller's emoji (e.g. "🐺") OR the agent_id (e.g. "cowork-bailey-lenovo"),
|
|
1212
|
+
// so we match against both. Pure broadcasts (only 'all') are excluded so
|
|
1213
|
+
// the lane stays a fast-path for things actually aimed at this agent.
|
|
1214
|
+
// The main 'messages' array is unchanged. The caller's emoji is resolved
|
|
1215
|
+
// from the 'profiles' array the API already returns, no extra lookup.
|
|
1216
|
+
if (name === "read_messages" &&
|
|
1217
|
+
resp.ok &&
|
|
1218
|
+
respBody &&
|
|
1219
|
+
typeof respBody === "object" &&
|
|
1220
|
+
Array.isArray(respBody.messages)) {
|
|
1221
|
+
const callerId = String(args.agent_id ?? "");
|
|
1222
|
+
const profiles = Array.isArray(respBody.profiles)
|
|
1223
|
+
? respBody.profiles
|
|
1224
|
+
: [];
|
|
1225
|
+
const callerProfile = profiles.find((p) => p.agent_id === callerId);
|
|
1226
|
+
const callerEmoji = callerProfile && typeof callerProfile.emoji === "string"
|
|
1227
|
+
? callerProfile.emoji
|
|
1228
|
+
: null;
|
|
1229
|
+
const messages = respBody.messages;
|
|
1230
|
+
const mentions = messages.filter((m) => {
|
|
1231
|
+
const recipients = Array.isArray(m.recipients) ? m.recipients : [];
|
|
1232
|
+
if (recipients.length === 0)
|
|
1233
|
+
return false;
|
|
1234
|
+
if (recipients.every((r) => r === "all"))
|
|
1235
|
+
return false;
|
|
1236
|
+
if (recipients.includes(callerId))
|
|
1237
|
+
return true;
|
|
1238
|
+
if (callerEmoji !== null && recipients.includes(callerEmoji))
|
|
1239
|
+
return true;
|
|
1240
|
+
return false;
|
|
1241
|
+
});
|
|
1242
|
+
respBody.mentions = mentions;
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
content: [{ type: "text", text: JSON.stringify(respBody, null, 2) }],
|
|
1246
|
+
isError: !resp.ok,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
563
1249
|
// ── UnClick Memory (direct tools + memory.* endpoints) ───────
|
|
564
|
-
|
|
565
|
-
|
|
1250
|
+
// Resolve new tool names (load_memory, save_fact, etc.) to canonical
|
|
1251
|
+
// handler keys (get_startup_context, add_fact, etc.). Old names still
|
|
1252
|
+
// work unchanged.
|
|
1253
|
+
const memoryKey = MEMORY_TOOL_ALIASES[name] ?? name;
|
|
1254
|
+
if (MEMORY_HANDLERS[memoryKey]) {
|
|
1255
|
+
const result = await MEMORY_HANDLERS[memoryKey](args);
|
|
566
1256
|
return {
|
|
567
1257
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
568
1258
|
};
|
|
@@ -602,7 +1292,7 @@ export function createServer() {
|
|
|
602
1292
|
for (const [cat, tools] of Object.entries(byCategory)) {
|
|
603
1293
|
lines.push(`## ${cat.toUpperCase()}`);
|
|
604
1294
|
for (const tool of tools) {
|
|
605
|
-
lines.push(`- **${tool.name}** (\`${tool.slug}\`)
|
|
1295
|
+
lines.push(`- **${tool.name}** (\`${tool.slug}\`) -- ${tool.description}`);
|
|
606
1296
|
}
|
|
607
1297
|
lines.push("");
|
|
608
1298
|
}
|
|
@@ -639,7 +1329,7 @@ export function createServer() {
|
|
|
639
1329
|
"## Endpoints",
|
|
640
1330
|
];
|
|
641
1331
|
for (const ep of tool.endpoints) {
|
|
642
|
-
lines.push(`### \`${ep.id}\`
|
|
1332
|
+
lines.push(`### \`${ep.id}\` -- ${ep.name}`);
|
|
643
1333
|
lines.push(ep.description);
|
|
644
1334
|
lines.push(`**Method:** ${ep.method} | **Path:** ${ep.path}`);
|
|
645
1335
|
lines.push(`**Input Schema:**`);
|
|
@@ -692,6 +1382,7 @@ export function createServer() {
|
|
|
692
1382
|
const additionalHandler = ADDITIONAL_HANDLERS[handlerKey];
|
|
693
1383
|
if (additionalHandler) {
|
|
694
1384
|
const result = await additionalHandler(params);
|
|
1385
|
+
signalToolFailure(handlerKey, result);
|
|
695
1386
|
return {
|
|
696
1387
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
697
1388
|
};
|
|
@@ -699,6 +1390,7 @@ export function createServer() {
|
|
|
699
1390
|
// Fall back to remote API for endpoints without local implementations
|
|
700
1391
|
const client = createClient();
|
|
701
1392
|
const result = await client.call(entry.endpoint.method, entry.endpoint.path, params);
|
|
1393
|
+
signalToolFailure(endpointId, result);
|
|
702
1394
|
return {
|
|
703
1395
|
content: [
|
|
704
1396
|
{
|
|
@@ -713,6 +1405,7 @@ export function createServer() {
|
|
|
713
1405
|
if (handler) {
|
|
714
1406
|
const client = createClient();
|
|
715
1407
|
const result = await handler(client, args);
|
|
1408
|
+
signalToolFailure(name, result);
|
|
716
1409
|
return {
|
|
717
1410
|
content: [
|
|
718
1411
|
{
|
|
@@ -726,6 +1419,7 @@ export function createServer() {
|
|
|
726
1419
|
const additionalHandler = ADDITIONAL_HANDLERS[name];
|
|
727
1420
|
if (additionalHandler) {
|
|
728
1421
|
const result = await additionalHandler(args);
|
|
1422
|
+
signalToolFailure(name, result);
|
|
729
1423
|
return {
|
|
730
1424
|
content: [
|
|
731
1425
|
{
|
|
@@ -741,7 +1435,37 @@ export function createServer() {
|
|
|
741
1435
|
};
|
|
742
1436
|
}
|
|
743
1437
|
catch (err) {
|
|
744
|
-
|
|
1438
|
+
let message;
|
|
1439
|
+
if (err instanceof Error) {
|
|
1440
|
+
message = err.message;
|
|
1441
|
+
}
|
|
1442
|
+
else if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
1443
|
+
// Postgres / Supabase / PostgREST errors are plain objects, not Error instances.
|
|
1444
|
+
// Surface code / details / hint when present so agents can self-diagnose.
|
|
1445
|
+
const e = err;
|
|
1446
|
+
const parts = [e.message];
|
|
1447
|
+
if (e.code)
|
|
1448
|
+
parts.push(`(code: ${e.code})`);
|
|
1449
|
+
if (e.details)
|
|
1450
|
+
parts.push(`details: ${e.details}`);
|
|
1451
|
+
if (e.hint)
|
|
1452
|
+
parts.push(`hint: ${e.hint}`);
|
|
1453
|
+
message = parts.join(" ");
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
message = String(err);
|
|
1457
|
+
}
|
|
1458
|
+
const apiKeyHash = currentApiKeyHash();
|
|
1459
|
+
if (apiKeyHash) {
|
|
1460
|
+
void emitSignal({
|
|
1461
|
+
apiKeyHash,
|
|
1462
|
+
tool: name,
|
|
1463
|
+
action: "exception",
|
|
1464
|
+
severity: "action_needed",
|
|
1465
|
+
summary: `${name}: ${message}`.slice(0, 500),
|
|
1466
|
+
payload: { source: "mcp-server" },
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
745
1469
|
return {
|
|
746
1470
|
content: [{ type: "text", text: `Error: ${message}` }],
|
|
747
1471
|
isError: true,
|
|
@@ -754,7 +1478,7 @@ export async function startServer() {
|
|
|
754
1478
|
const server = createServer();
|
|
755
1479
|
const transport = new StdioServerTransport();
|
|
756
1480
|
await server.connect(transport);
|
|
757
|
-
// Server is running
|
|
1481
|
+
// Server is running -- errors go to stderr so they don't corrupt the MCP stream
|
|
758
1482
|
process.stderr.write("UnClick MCP server running on stdio\n");
|
|
759
1483
|
}
|
|
760
1484
|
//# sourceMappingURL=server.js.map
|