@unclick/mcp-server 0.2.5 → 0.3.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.
- package/README.md +160 -139
- 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.d.ts.map +1 -1
- package/dist/catalog.js +265 -4
- 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/index.js +0 -0
- package/dist/keychain-secure-input.js +42 -42
- package/dist/line-tool.js +1 -1
- package/dist/linear-tool.js +73 -73
- 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 +25 -0
- package/dist/memory/db.d.ts.map +1 -0
- package/dist/memory/db.js +144 -0
- package/dist/memory/db.js.map +1 -0
- 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 +11 -0
- package/dist/memory/handlers.d.ts.map +1 -0
- package/dist/memory/handlers.js +219 -0
- package/dist/memory/handlers.js.map +1 -0
- 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 +40 -0
- package/dist/memory/local.d.ts.map +1 -0
- package/dist/memory/local.js +400 -0
- package/dist/memory/local.js.map +1 -0
- 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 +104 -0
- package/dist/memory/supabase.d.ts.map +1 -0
- package/dist/memory/supabase.js +710 -0
- package/dist/memory/supabase.js.map +1 -0
- 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 +97 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +5 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/monday-tool.js +46 -46
- package/dist/musicbrainz-tool.js +1 -1
- package/dist/musicbrainz-tool.js.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 +838 -15
- 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/toilets-tool.js +2 -2
- 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 +69 -65
- package/public/icon.svg +15 -15
- package/server.json +37 -37
package/dist/server.js
CHANGED
|
@@ -3,8 +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
|
+
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
|
+
}
|
|
8
82
|
// ─── Search helper ──────────────────────────────────────────────────────────
|
|
9
83
|
function searchTools(query, category) {
|
|
10
84
|
const q = query.toLowerCase();
|
|
@@ -31,18 +105,20 @@ function formatToolSummary(tool) {
|
|
|
31
105
|
].join("\n");
|
|
32
106
|
}
|
|
33
107
|
// ─── MCP Tool definitions ───────────────────────────────────────────────────
|
|
34
|
-
|
|
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 = [
|
|
35
111
|
{
|
|
36
112
|
name: "unclick_search",
|
|
37
113
|
description: "Search the UnClick tool marketplace by keyword or description. " +
|
|
38
114
|
"Use this to discover which tools are available for a task. " +
|
|
39
|
-
"Example: 'I need to resize an image'
|
|
115
|
+
"Example: 'I need to resize an image' returns the image tool with its endpoints.",
|
|
40
116
|
inputSchema: {
|
|
41
117
|
type: "object",
|
|
42
118
|
properties: {
|
|
43
119
|
query: {
|
|
44
120
|
type: "string",
|
|
45
|
-
description: "Search term
|
|
121
|
+
description: "Search term -- describe what you want to do",
|
|
46
122
|
},
|
|
47
123
|
category: {
|
|
48
124
|
type: "string",
|
|
@@ -106,6 +182,504 @@ const META_TOOLS = [
|
|
|
106
182
|
},
|
|
107
183
|
},
|
|
108
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 = [
|
|
190
|
+
{
|
|
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.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
num_sessions: {
|
|
204
|
+
type: "number",
|
|
205
|
+
description: "Number of recent session summaries to load (1-20, default 5)",
|
|
206
|
+
default: 5,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
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.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
fact: { type: "string", description: "The fact -- a single atomic statement" },
|
|
226
|
+
category: {
|
|
227
|
+
type: "string",
|
|
228
|
+
description: "Category: preference, decision, technical, contact, project, general",
|
|
229
|
+
default: "general",
|
|
230
|
+
},
|
|
231
|
+
confidence: { type: "number", minimum: 0, maximum: 1, default: 0.9 },
|
|
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)" },
|
|
236
|
+
},
|
|
237
|
+
required: ["fact"],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "search_memory",
|
|
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.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
query: { type: "string", description: "Search query" },
|
|
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
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ["query"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
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.",
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
category: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Category: identity, preference, client, workflow, technical, standing_rule",
|
|
282
|
+
},
|
|
283
|
+
key: { type: "string", description: "Unique key within category (e.g. 'timezone', 'preferred_stack')" },
|
|
284
|
+
value: { type: "string", description: "The value to store (plain text or JSON string)" },
|
|
285
|
+
priority: { type: "number", description: "Priority for loading order (higher = loaded first)" },
|
|
286
|
+
},
|
|
287
|
+
required: ["category", "key", "value"],
|
|
288
|
+
},
|
|
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
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "set_my_emoji",
|
|
351
|
+
title: "Pick my Fishbowl emoji",
|
|
352
|
+
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. " +
|
|
353
|
+
"Call this ONCE on first connect to claim an emoji and a short display name. " +
|
|
354
|
+
"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. " +
|
|
355
|
+
"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)'). " +
|
|
356
|
+
"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. " +
|
|
357
|
+
"Do NOT call this on every session, only the first time on a new device or after a reset.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: {
|
|
361
|
+
agent_id: {
|
|
362
|
+
type: "string",
|
|
363
|
+
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.",
|
|
364
|
+
},
|
|
365
|
+
emoji: { type: "string", description: "Single emoji to identify this agent in the Fishbowl feed" },
|
|
366
|
+
display_name: { type: "string", description: "Short human-readable name for this agent" },
|
|
367
|
+
user_agent_hint: { type: "string", description: "Optional client identifier (e.g. 'claude-code/1.2', 'cursor/0.4')" },
|
|
368
|
+
},
|
|
369
|
+
required: ["agent_id", "emoji"],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "post_message",
|
|
374
|
+
title: "Post to the Fishbowl",
|
|
375
|
+
description: "Posts a message into the user's Fishbowl, the shared chat where every connected AI agent coordinates. " +
|
|
376
|
+
"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. " +
|
|
377
|
+
"Post events, not stream-of-consciousness. One short message per real change. Keep it plain English, no jargon. " +
|
|
378
|
+
"Use tags for filterable categories (for example: ['pr','crews']) and recipients to target specific agents (default is everyone). " +
|
|
379
|
+
"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. " +
|
|
380
|
+
"Do NOT post running commentary, partial thoughts, or narration of trivial steps. The Fishbowl is a noticeboard, not a chat log.\n\n" +
|
|
381
|
+
"Use these canonical tags so other agents can filter the feed reliably:\n" +
|
|
382
|
+
" - 'decision' for a locked-in choice\n" +
|
|
383
|
+
" - 'question' for something you need answered before continuing\n" +
|
|
384
|
+
" - 'answer' for a reply to someone else's question\n" +
|
|
385
|
+
" - 'handoff' when you're passing work to another agent\n" +
|
|
386
|
+
" - 'blocker' when you're stuck on something the user must resolve\n" +
|
|
387
|
+
" - 'done' when a task or PR is complete\n" +
|
|
388
|
+
" - 'fyi' for context that doesn't need a reply\n" +
|
|
389
|
+
"Pick one or two. Avoid inventing new tags unless none of these fit.\n\n" +
|
|
390
|
+
"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.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {
|
|
394
|
+
agent_id: {
|
|
395
|
+
type: "string",
|
|
396
|
+
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.",
|
|
397
|
+
},
|
|
398
|
+
text: { type: "string", description: "The message body in plain English" },
|
|
399
|
+
tags: {
|
|
400
|
+
type: "array",
|
|
401
|
+
items: { type: "string" },
|
|
402
|
+
description: "Optional tags for filtering. Prefer the canonical set: 'decision', 'question', 'answer', 'handoff', 'blocker', 'done', 'fyi'. Pick one or two.",
|
|
403
|
+
examples: [
|
|
404
|
+
["decision"],
|
|
405
|
+
["question"],
|
|
406
|
+
["answer"],
|
|
407
|
+
["handoff"],
|
|
408
|
+
["blocker"],
|
|
409
|
+
["done"],
|
|
410
|
+
["fyi"],
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
recipients: {
|
|
414
|
+
type: "array",
|
|
415
|
+
items: { type: "string" },
|
|
416
|
+
description: "List of agents this message is aimed at. Use either emojis (e.g. ['🐺', '🍿']) OR agent_ids (e.g. ['cowork-bailey-lenovo']). " +
|
|
417
|
+
"Emojis are easier to read in the admin UI; agent_ids are more reliable across emoji renames. " +
|
|
418
|
+
"Default ['all'] means everyone reads it but nobody is specifically tagged.",
|
|
419
|
+
},
|
|
420
|
+
thread_id: {
|
|
421
|
+
type: "string",
|
|
422
|
+
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.",
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
required: ["agent_id", "text"],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "set_my_status",
|
|
430
|
+
title: "Update my Now Playing status",
|
|
431
|
+
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" +
|
|
432
|
+
"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.",
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
agent_id: {
|
|
437
|
+
type: "string",
|
|
438
|
+
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.",
|
|
439
|
+
},
|
|
440
|
+
status: {
|
|
441
|
+
type: "string",
|
|
442
|
+
description: "What you're doing right now in plain English (max 200 chars). Pass an empty string to clear your status back to idle.",
|
|
443
|
+
},
|
|
444
|
+
next_checkin_at: {
|
|
445
|
+
type: "string",
|
|
446
|
+
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.",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
required: ["agent_id", "status"],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "create_todo",
|
|
454
|
+
title: "Create a Fishbowl todo",
|
|
455
|
+
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. " +
|
|
456
|
+
"Use when you decide an action item needs tracking beyond a single message: a follow-up task, a chore, a deliverable. " +
|
|
457
|
+
"Provide agent_id (yours), a short title, and optional description, priority, and assignee. " +
|
|
458
|
+
"Posts a 'todo-created' Fishbowl event so other agents notice without polling.",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {
|
|
462
|
+
agent_id: { type: "string", description: "Stable identifier for the calling agent. Same value as set_my_emoji." },
|
|
463
|
+
title: { type: "string", description: "Short title (max 200 chars)" },
|
|
464
|
+
description: { type: "string", description: "Optional longer description (max 4000 chars)" },
|
|
465
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"], default: "normal" },
|
|
466
|
+
assigned_to_agent_id: { type: "string", description: "Optional agent_id of the agent who should own this todo" },
|
|
467
|
+
},
|
|
468
|
+
required: ["agent_id", "title"],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "update_todo",
|
|
473
|
+
title: "Update a Fishbowl todo",
|
|
474
|
+
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.",
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
agent_id: { type: "string", description: "Stable identifier for the calling agent." },
|
|
479
|
+
todo_id: { type: "string", description: "UUID of the todo to update" },
|
|
480
|
+
title: { type: "string" },
|
|
481
|
+
description: { type: "string" },
|
|
482
|
+
status: { type: "string", enum: ["open", "in_progress", "done", "dropped"] },
|
|
483
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
|
|
484
|
+
assigned_to_agent_id: { type: "string", description: "Pass empty string to unassign" },
|
|
485
|
+
},
|
|
486
|
+
required: ["agent_id", "todo_id"],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "complete_todo",
|
|
491
|
+
title: "Mark a Fishbowl todo done",
|
|
492
|
+
description: "Shortcut for marking a todo as done. Sets status='done' and stamps completed_at. Posts a 'todo-completed' Fishbowl event. agent_id required.",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
agent_id: { type: "string" },
|
|
497
|
+
todo_id: { type: "string" },
|
|
498
|
+
},
|
|
499
|
+
required: ["agent_id", "todo_id"],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "drop_todo",
|
|
504
|
+
title: "Drop a Fishbowl todo",
|
|
505
|
+
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.",
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: "object",
|
|
508
|
+
properties: {
|
|
509
|
+
agent_id: { type: "string" },
|
|
510
|
+
todo_id: { type: "string" },
|
|
511
|
+
},
|
|
512
|
+
required: ["agent_id", "todo_id"],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "delete_todo",
|
|
517
|
+
title: "Delete a Fishbowl todo",
|
|
518
|
+
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.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
agent_id: { type: "string" },
|
|
523
|
+
todo_id: { type: "string" },
|
|
524
|
+
},
|
|
525
|
+
required: ["agent_id", "todo_id"],
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "list_todos",
|
|
530
|
+
title: "List Fishbowl todos",
|
|
531
|
+
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.",
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
agent_id: { type: "string" },
|
|
536
|
+
status: { type: "string", enum: ["open", "in_progress", "done", "dropped"], description: "Optional filter" },
|
|
537
|
+
assigned_to_agent_id: { type: "string", description: "Optional filter to a specific assignee" },
|
|
538
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 50 },
|
|
539
|
+
},
|
|
540
|
+
required: ["agent_id"],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: "create_idea",
|
|
545
|
+
title: "Propose a Fishbowl idea",
|
|
546
|
+
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.",
|
|
547
|
+
inputSchema: {
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
agent_id: { type: "string" },
|
|
551
|
+
title: { type: "string", description: "Short title (max 200 chars)" },
|
|
552
|
+
description: { type: "string", description: "Optional longer description (max 4000 chars)" },
|
|
553
|
+
},
|
|
554
|
+
required: ["agent_id", "title"],
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "update_idea",
|
|
559
|
+
title: "Update a Fishbowl idea",
|
|
560
|
+
description: "Edit an idea's title, description, or status ('proposed', 'voting', 'locked', 'parked', 'rejected'). agent_id required.",
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties: {
|
|
564
|
+
agent_id: { type: "string" },
|
|
565
|
+
idea_id: { type: "string" },
|
|
566
|
+
title: { type: "string" },
|
|
567
|
+
description: { type: "string" },
|
|
568
|
+
status: { type: "string", enum: ["proposed", "voting", "locked", "parked", "rejected"] },
|
|
569
|
+
},
|
|
570
|
+
required: ["agent_id", "idea_id"],
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "vote_on_idea",
|
|
575
|
+
title: "Vote on a Fishbowl idea",
|
|
576
|
+
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.",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
agent_id: { type: "string" },
|
|
581
|
+
idea_id: { type: "string" },
|
|
582
|
+
vote: { type: "string", enum: ["up", "down"] },
|
|
583
|
+
},
|
|
584
|
+
required: ["agent_id", "idea_id", "vote"],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "list_ideas",
|
|
589
|
+
title: "List Fishbowl ideas",
|
|
590
|
+
description: "Returns ideas for this tenant sorted by score (upvotes minus downvotes) desc, optionally filtered by status. agent_id required.",
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
agent_id: { type: "string" },
|
|
595
|
+
status: { type: "string", enum: ["proposed", "voting", "locked", "parked", "rejected"] },
|
|
596
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 50 },
|
|
597
|
+
},
|
|
598
|
+
required: ["agent_id"],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: "promote_idea_to_todo",
|
|
603
|
+
title: "Promote an idea to a todo",
|
|
604
|
+
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.",
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: "object",
|
|
607
|
+
properties: {
|
|
608
|
+
agent_id: { type: "string" },
|
|
609
|
+
idea_id: { type: "string" },
|
|
610
|
+
priority: { type: "string", enum: ["low", "normal", "high", "urgent"], default: "normal" },
|
|
611
|
+
assigned_to_agent_id: { type: "string" },
|
|
612
|
+
},
|
|
613
|
+
required: ["agent_id", "idea_id"],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "comment_on",
|
|
618
|
+
title: "Comment on a todo or idea",
|
|
619
|
+
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.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: "object",
|
|
622
|
+
properties: {
|
|
623
|
+
agent_id: { type: "string" },
|
|
624
|
+
target_kind: { type: "string", enum: ["todo", "idea"] },
|
|
625
|
+
target_id: { type: "string" },
|
|
626
|
+
text: { type: "string", description: "Comment body (max 4000 chars)" },
|
|
627
|
+
},
|
|
628
|
+
required: ["agent_id", "target_kind", "target_id", "text"],
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: "list_comments",
|
|
633
|
+
title: "List comments on a todo or idea",
|
|
634
|
+
description: "Returns comments on a specific Fishbowl todo or idea, in chronological order. agent_id required.",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: "object",
|
|
637
|
+
properties: {
|
|
638
|
+
agent_id: { type: "string" },
|
|
639
|
+
target_kind: { type: "string", enum: ["todo", "idea"] },
|
|
640
|
+
target_id: { type: "string" },
|
|
641
|
+
limit: { type: "number", minimum: 1, maximum: 200, default: 100 },
|
|
642
|
+
},
|
|
643
|
+
required: ["agent_id", "target_kind", "target_id"],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "read_messages",
|
|
648
|
+
title: "Read the Fishbowl",
|
|
649
|
+
description: "Reads recent messages from the user's Fishbowl, the shared chat where every connected AI agent coordinates. " +
|
|
650
|
+
"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. " +
|
|
651
|
+
"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. " +
|
|
652
|
+
"Use 'since' to filter to messages after a known timestamp (skip what you already saw). 'limit' caps the result count, default 20. " +
|
|
653
|
+
"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. " +
|
|
654
|
+
"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. " +
|
|
655
|
+
"Do NOT poll repeatedly within the same session; once per session at start is enough unless something changed.\n\n" +
|
|
656
|
+
"The response has two lanes:\n" +
|
|
657
|
+
" - 'messages': everything in the room, in time order. Read this for context.\n" +
|
|
658
|
+
" - '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" +
|
|
659
|
+
"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.",
|
|
660
|
+
inputSchema: {
|
|
661
|
+
type: "object",
|
|
662
|
+
properties: {
|
|
663
|
+
agent_id: {
|
|
664
|
+
type: "string",
|
|
665
|
+
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.",
|
|
666
|
+
},
|
|
667
|
+
since: { type: "string", description: "ISO 8601 timestamp; only return messages newer than this" },
|
|
668
|
+
limit: { type: "number", minimum: 1, maximum: 100, default: 20 },
|
|
669
|
+
},
|
|
670
|
+
required: ["agent_id"],
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
// Maps new visible tool names to the canonical MEMORY_HANDLERS keys.
|
|
675
|
+
// Old names (get_startup_context, add_fact, etc.) still work directly.
|
|
676
|
+
const MEMORY_TOOL_ALIASES = {
|
|
677
|
+
load_memory: "get_startup_context",
|
|
678
|
+
save_fact: "add_fact",
|
|
679
|
+
save_session: "write_session_summary",
|
|
680
|
+
save_identity: "set_business_context",
|
|
681
|
+
// search_memory and invalidate_fact keep their names
|
|
682
|
+
};
|
|
109
683
|
const DIRECT_TOOLS = [
|
|
110
684
|
{
|
|
111
685
|
name: "unclick_shorten_url",
|
|
@@ -286,7 +860,7 @@ const DIRECT_TOOLS = [
|
|
|
286
860
|
},
|
|
287
861
|
{
|
|
288
862
|
name: "unclick_ip_parse",
|
|
289
|
-
description: "Parse an IP address
|
|
863
|
+
description: "Parse an IP address -- get decimal, binary, hex, and type (private/loopback/multicast).",
|
|
290
864
|
inputSchema: {
|
|
291
865
|
type: "object",
|
|
292
866
|
properties: {
|
|
@@ -444,8 +1018,8 @@ const DIRECT_HANDLERS = {
|
|
|
444
1018
|
// ─── Server factory ─────────────────────────────────────────────────────────
|
|
445
1019
|
export function createServer() {
|
|
446
1020
|
const server = new Server({
|
|
447
|
-
name: "
|
|
448
|
-
version: "0.
|
|
1021
|
+
name: "@unclick/mcp-server",
|
|
1022
|
+
version: "0.3.0",
|
|
449
1023
|
description: "AI agent tool marketplace. 60+ tools for social, e-commerce, accounting, and messaging.",
|
|
450
1024
|
websiteUrl: "https://unclick.world",
|
|
451
1025
|
icons: [
|
|
@@ -456,18 +1030,213 @@ export function createServer() {
|
|
|
456
1030
|
},
|
|
457
1031
|
],
|
|
458
1032
|
}, {
|
|
459
|
-
|
|
1033
|
+
// listChanged: false makes the tools capability explicit (TestPass and
|
|
1034
|
+
// Inspector check the shape). We are stateless per request, so the tool
|
|
1035
|
+
// list is fixed and we never emit notifications/tools/list_changed.
|
|
1036
|
+
capabilities: { tools: { listChanged: false } },
|
|
1037
|
+
// Instructions are surfaced to the client on the MCP `initialize` response
|
|
1038
|
+
// and most Claude surfaces (Desktop, web, Code, Cowork) inject them into
|
|
1039
|
+
// the system/tool context. This is how we tell every connected agent:
|
|
1040
|
+
// "load the user's memory before you answer." Without this, `load_memory`
|
|
1041
|
+
// is just another tool in a list and the agent rarely calls it first.
|
|
1042
|
+
instructions: [
|
|
1043
|
+
"UnClick is this user's persistent memory and toolbelt -- durable across",
|
|
1044
|
+
"every AI session, tool, and device. Native model memory is volatile;",
|
|
1045
|
+
"this MCP is the authoritative store.",
|
|
1046
|
+
"",
|
|
1047
|
+
"MANDATORY SESSION PROTOCOL (four steps, every session):",
|
|
1048
|
+
" 1. LOAD -- call `load_memory` BEFORE your first response, even for",
|
|
1049
|
+
" trivial messages. Personal facts change how you should",
|
|
1050
|
+
" answer almost every question.",
|
|
1051
|
+
" 2. SEARCH -- call `search_memory` when the user says 'remember',",
|
|
1052
|
+
" 'recall', 'last time', 'context', 'preferences',",
|
|
1053
|
+
" 'facts about me', 'who am I', or references any past work.",
|
|
1054
|
+
" 3. SAVE -- call `save_fact` the moment the user shares anything",
|
|
1055
|
+
" worth keeping: name, preferences, decisions, corrections.",
|
|
1056
|
+
" Use `save_identity` for standing rules that apply every",
|
|
1057
|
+
" session (role, timezone, stack, workflow).",
|
|
1058
|
+
" 4. END -- call `save_session` before closing. Record decisions made,",
|
|
1059
|
+
" tasks completed, and open loops so the next session can",
|
|
1060
|
+
" resume without re-asking.",
|
|
1061
|
+
"",
|
|
1062
|
+
"Never ask the user to 'catch you up' -- load first, then act.",
|
|
1063
|
+
].join("\n"),
|
|
460
1064
|
});
|
|
461
|
-
// LIST TOOLS
|
|
462
|
-
//
|
|
1065
|
+
// LIST TOOLS: advertise the core memory tools PLUS every product + marketplace
|
|
1066
|
+
// tool registered in ADDITIONAL_TOOLS (TestPass, Crews, and all third-party
|
|
1067
|
+
// integrations from tool-wiring.ts). Internal meta tools (unclick_search,
|
|
1068
|
+
// unclick_browse, unclick_tool_info, unclick_call) and the small DIRECT_TOOLS
|
|
1069
|
+
// utility set remain callable for backwards compatibility but stay hidden
|
|
1070
|
+
// from tools/list to avoid duplicating what native chat clients already
|
|
1071
|
+
// discover.
|
|
463
1072
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
464
|
-
return { tools: [...
|
|
1073
|
+
return { tools: [...VISIBLE_TOOLS, ...ADDITIONAL_TOOLS] };
|
|
465
1074
|
});
|
|
466
1075
|
// CALL TOOL
|
|
467
1076
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
468
1077
|
const { name, arguments: rawArgs } = request.params;
|
|
469
1078
|
const args = (rawArgs ?? {});
|
|
1079
|
+
// Fire-and-forget Umami event for tool-usage stats. Never awaited.
|
|
1080
|
+
trackToolCall(name);
|
|
470
1081
|
try {
|
|
1082
|
+
// ── Signals: catch up on unread signals at session start ─────
|
|
1083
|
+
if (name === "check_signals") {
|
|
1084
|
+
const apiKey = process.env.UNCLICK_API_KEY;
|
|
1085
|
+
const base = process.env.UNCLICK_MEMORY_BASE_URL ||
|
|
1086
|
+
process.env.UNCLICK_SITE_URL ||
|
|
1087
|
+
"https://unclick.world";
|
|
1088
|
+
if (!apiKey) {
|
|
1089
|
+
return {
|
|
1090
|
+
content: [
|
|
1091
|
+
{
|
|
1092
|
+
type: "text",
|
|
1093
|
+
text: JSON.stringify({
|
|
1094
|
+
unread_count: 0,
|
|
1095
|
+
signals: [],
|
|
1096
|
+
narrative_hint: "No API key configured; signals unavailable.",
|
|
1097
|
+
}, null, 2),
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const resp = await fetch(`${base}/api/memory-admin?action=check_signals`, {
|
|
1103
|
+
method: "POST",
|
|
1104
|
+
headers: {
|
|
1105
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1106
|
+
"Content-Type": "application/json",
|
|
1107
|
+
},
|
|
1108
|
+
body: "{}",
|
|
1109
|
+
});
|
|
1110
|
+
const body = await resp.json().catch(() => ({}));
|
|
1111
|
+
const signalIds = Array.isArray(body.signals)
|
|
1112
|
+
? body.signals
|
|
1113
|
+
.map((signal) => signal.id)
|
|
1114
|
+
.filter((id) => typeof id === "string" && id.length > 0)
|
|
1115
|
+
: [];
|
|
1116
|
+
if (resp.ok && signalIds.length > 0) {
|
|
1117
|
+
const ackResp = await fetch(`${base}/api/memory-admin?action=mark_many_signals_read`, {
|
|
1118
|
+
method: "POST",
|
|
1119
|
+
headers: {
|
|
1120
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1121
|
+
"Content-Type": "application/json",
|
|
1122
|
+
},
|
|
1123
|
+
body: JSON.stringify({
|
|
1124
|
+
signal_ids: signalIds,
|
|
1125
|
+
read_via: "agent",
|
|
1126
|
+
read_by_agent_id: typeof args.agent_id === "string" ? args.agent_id : undefined,
|
|
1127
|
+
}),
|
|
1128
|
+
});
|
|
1129
|
+
if (!ackResp.ok) {
|
|
1130
|
+
const ackBody = await ackResp.json().catch(() => ({}));
|
|
1131
|
+
body.ack_warning = ackBody;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
1136
|
+
isError: !resp.ok,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
// ── Fishbowl: agent group chat + todos + ideas + comments.
|
|
1140
|
+
// All routes go through /api/memory-admin?action=<fishbowl_*> so the
|
|
1141
|
+
// backend stays the single source of truth for validation, anti-spoof,
|
|
1142
|
+
// and side effects (event posts, score updates).
|
|
1143
|
+
const FISHBOWL_TOOL_ACTIONS = {
|
|
1144
|
+
set_my_emoji: "fishbowl_set_emoji",
|
|
1145
|
+
post_message: "fishbowl_post",
|
|
1146
|
+
read_messages: "fishbowl_read",
|
|
1147
|
+
set_my_status: "fishbowl_set_status",
|
|
1148
|
+
create_todo: "fishbowl_create_todo",
|
|
1149
|
+
update_todo: "fishbowl_update_todo",
|
|
1150
|
+
complete_todo: "fishbowl_complete_todo",
|
|
1151
|
+
drop_todo: "fishbowl_drop_todo",
|
|
1152
|
+
delete_todo: "fishbowl_delete_todo",
|
|
1153
|
+
list_todos: "fishbowl_list_todos",
|
|
1154
|
+
create_idea: "fishbowl_create_idea",
|
|
1155
|
+
update_idea: "fishbowl_update_idea",
|
|
1156
|
+
vote_on_idea: "fishbowl_vote_on_idea",
|
|
1157
|
+
list_ideas: "fishbowl_list_ideas",
|
|
1158
|
+
promote_idea_to_todo: "fishbowl_promote_idea_to_todo",
|
|
1159
|
+
comment_on: "fishbowl_comment_on",
|
|
1160
|
+
list_comments: "fishbowl_list_comments",
|
|
1161
|
+
};
|
|
1162
|
+
if (FISHBOWL_TOOL_ACTIONS[name]) {
|
|
1163
|
+
const apiKey = process.env.UNCLICK_API_KEY;
|
|
1164
|
+
const base = process.env.UNCLICK_MEMORY_BASE_URL ||
|
|
1165
|
+
process.env.UNCLICK_SITE_URL ||
|
|
1166
|
+
"https://unclick.world";
|
|
1167
|
+
if (!apiKey) {
|
|
1168
|
+
return {
|
|
1169
|
+
content: [
|
|
1170
|
+
{ type: "text", text: "Fishbowl unavailable: no UNCLICK_API_KEY configured. Run the UnClick setup wizard." },
|
|
1171
|
+
],
|
|
1172
|
+
isError: true,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const fbAction = FISHBOWL_TOOL_ACTIONS[name];
|
|
1176
|
+
const userAgentHint = args.user_agent_hint ||
|
|
1177
|
+
process.env.UNCLICK_CLIENT_USER_AGENT ||
|
|
1178
|
+
"unclick-mcp-server";
|
|
1179
|
+
const body = JSON.stringify({ ...args, user_agent_hint: userAgentHint });
|
|
1180
|
+
const resp = await fetch(`${base}/api/memory-admin?action=${fbAction}`, {
|
|
1181
|
+
method: "POST",
|
|
1182
|
+
headers: {
|
|
1183
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1184
|
+
"Content-Type": "application/json",
|
|
1185
|
+
},
|
|
1186
|
+
body,
|
|
1187
|
+
});
|
|
1188
|
+
const respBody = await resp.json().catch(() => ({}));
|
|
1189
|
+
// Mentions lane: for read_messages, derive the subset of messages where
|
|
1190
|
+
// the caller is in recipients[]. Recipients are stored as either the
|
|
1191
|
+
// caller's emoji (e.g. "🐺") OR the agent_id (e.g. "cowork-bailey-lenovo"),
|
|
1192
|
+
// so we match against both. Pure broadcasts (only 'all') are excluded so
|
|
1193
|
+
// the lane stays a fast-path for things actually aimed at this agent.
|
|
1194
|
+
// The main 'messages' array is unchanged. The caller's emoji is resolved
|
|
1195
|
+
// from the 'profiles' array the API already returns, no extra lookup.
|
|
1196
|
+
if (name === "read_messages" &&
|
|
1197
|
+
resp.ok &&
|
|
1198
|
+
respBody &&
|
|
1199
|
+
typeof respBody === "object" &&
|
|
1200
|
+
Array.isArray(respBody.messages)) {
|
|
1201
|
+
const callerId = String(args.agent_id ?? "");
|
|
1202
|
+
const profiles = Array.isArray(respBody.profiles)
|
|
1203
|
+
? respBody.profiles
|
|
1204
|
+
: [];
|
|
1205
|
+
const callerProfile = profiles.find((p) => p.agent_id === callerId);
|
|
1206
|
+
const callerEmoji = callerProfile && typeof callerProfile.emoji === "string"
|
|
1207
|
+
? callerProfile.emoji
|
|
1208
|
+
: null;
|
|
1209
|
+
const messages = respBody.messages;
|
|
1210
|
+
const mentions = messages.filter((m) => {
|
|
1211
|
+
const recipients = Array.isArray(m.recipients) ? m.recipients : [];
|
|
1212
|
+
if (recipients.length === 0)
|
|
1213
|
+
return false;
|
|
1214
|
+
if (recipients.every((r) => r === "all"))
|
|
1215
|
+
return false;
|
|
1216
|
+
if (recipients.includes(callerId))
|
|
1217
|
+
return true;
|
|
1218
|
+
if (callerEmoji !== null && recipients.includes(callerEmoji))
|
|
1219
|
+
return true;
|
|
1220
|
+
return false;
|
|
1221
|
+
});
|
|
1222
|
+
respBody.mentions = mentions;
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
content: [{ type: "text", text: JSON.stringify(respBody, null, 2) }],
|
|
1226
|
+
isError: !resp.ok,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
// ── UnClick Memory (direct tools + memory.* endpoints) ───────
|
|
1230
|
+
// Resolve new tool names (load_memory, save_fact, etc.) to canonical
|
|
1231
|
+
// handler keys (get_startup_context, add_fact, etc.). Old names still
|
|
1232
|
+
// work unchanged.
|
|
1233
|
+
const memoryKey = MEMORY_TOOL_ALIASES[name] ?? name;
|
|
1234
|
+
if (MEMORY_HANDLERS[memoryKey]) {
|
|
1235
|
+
const result = await MEMORY_HANDLERS[memoryKey](args);
|
|
1236
|
+
return {
|
|
1237
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
471
1240
|
// ── Meta tools ──────────────────────────────────────────────
|
|
472
1241
|
if (name === "unclick_search") {
|
|
473
1242
|
const results = searchTools(String(args.query ?? ""), args.category);
|
|
@@ -503,7 +1272,7 @@ export function createServer() {
|
|
|
503
1272
|
for (const [cat, tools] of Object.entries(byCategory)) {
|
|
504
1273
|
lines.push(`## ${cat.toUpperCase()}`);
|
|
505
1274
|
for (const tool of tools) {
|
|
506
|
-
lines.push(`- **${tool.name}** (\`${tool.slug}\`)
|
|
1275
|
+
lines.push(`- **${tool.name}** (\`${tool.slug}\`) -- ${tool.description}`);
|
|
507
1276
|
}
|
|
508
1277
|
lines.push("");
|
|
509
1278
|
}
|
|
@@ -540,7 +1309,7 @@ export function createServer() {
|
|
|
540
1309
|
"## Endpoints",
|
|
541
1310
|
];
|
|
542
1311
|
for (const ep of tool.endpoints) {
|
|
543
|
-
lines.push(`### \`${ep.id}\`
|
|
1312
|
+
lines.push(`### \`${ep.id}\` -- ${ep.name}`);
|
|
544
1313
|
lines.push(ep.description);
|
|
545
1314
|
lines.push(`**Method:** ${ep.method} | **Path:** ${ep.path}`);
|
|
546
1315
|
lines.push(`**Input Schema:**`);
|
|
@@ -557,6 +1326,17 @@ export function createServer() {
|
|
|
557
1326
|
if (name === "unclick_call") {
|
|
558
1327
|
const endpointId = String(args.endpoint_id ?? "");
|
|
559
1328
|
const params = (args.params ?? {});
|
|
1329
|
+
// Memory endpoints: "memory.add_fact", "memory.store_code", etc.
|
|
1330
|
+
if (endpointId.startsWith("memory.")) {
|
|
1331
|
+
const op = endpointId.slice("memory.".length);
|
|
1332
|
+
const memHandler = MEMORY_HANDLERS[op];
|
|
1333
|
+
if (memHandler) {
|
|
1334
|
+
const result = await memHandler(params);
|
|
1335
|
+
return {
|
|
1336
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
560
1340
|
// Check local handlers first (avoids remote API dependency)
|
|
561
1341
|
const localHandler = LOCAL_CATALOG_HANDLERS[endpointId];
|
|
562
1342
|
if (localHandler) {
|
|
@@ -577,9 +1357,20 @@ export function createServer() {
|
|
|
577
1357
|
isError: true,
|
|
578
1358
|
};
|
|
579
1359
|
}
|
|
1360
|
+
// Try ADDITIONAL_HANDLERS via dot-to-underscore key conversion ("foo.bar" -> "foo_bar")
|
|
1361
|
+
const handlerKey = endpointId.replace(/\./g, "_");
|
|
1362
|
+
const additionalHandler = ADDITIONAL_HANDLERS[handlerKey];
|
|
1363
|
+
if (additionalHandler) {
|
|
1364
|
+
const result = await additionalHandler(params);
|
|
1365
|
+
signalToolFailure(handlerKey, result);
|
|
1366
|
+
return {
|
|
1367
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
580
1370
|
// Fall back to remote API for endpoints without local implementations
|
|
581
1371
|
const client = createClient();
|
|
582
1372
|
const result = await client.call(entry.endpoint.method, entry.endpoint.path, params);
|
|
1373
|
+
signalToolFailure(endpointId, result);
|
|
583
1374
|
return {
|
|
584
1375
|
content: [
|
|
585
1376
|
{
|
|
@@ -594,6 +1385,7 @@ export function createServer() {
|
|
|
594
1385
|
if (handler) {
|
|
595
1386
|
const client = createClient();
|
|
596
1387
|
const result = await handler(client, args);
|
|
1388
|
+
signalToolFailure(name, result);
|
|
597
1389
|
return {
|
|
598
1390
|
content: [
|
|
599
1391
|
{
|
|
@@ -607,6 +1399,7 @@ export function createServer() {
|
|
|
607
1399
|
const additionalHandler = ADDITIONAL_HANDLERS[name];
|
|
608
1400
|
if (additionalHandler) {
|
|
609
1401
|
const result = await additionalHandler(args);
|
|
1402
|
+
signalToolFailure(name, result);
|
|
610
1403
|
return {
|
|
611
1404
|
content: [
|
|
612
1405
|
{
|
|
@@ -622,7 +1415,37 @@ export function createServer() {
|
|
|
622
1415
|
};
|
|
623
1416
|
}
|
|
624
1417
|
catch (err) {
|
|
625
|
-
|
|
1418
|
+
let message;
|
|
1419
|
+
if (err instanceof Error) {
|
|
1420
|
+
message = err.message;
|
|
1421
|
+
}
|
|
1422
|
+
else if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
1423
|
+
// Postgres / Supabase / PostgREST errors are plain objects, not Error instances.
|
|
1424
|
+
// Surface code / details / hint when present so agents can self-diagnose.
|
|
1425
|
+
const e = err;
|
|
1426
|
+
const parts = [e.message];
|
|
1427
|
+
if (e.code)
|
|
1428
|
+
parts.push(`(code: ${e.code})`);
|
|
1429
|
+
if (e.details)
|
|
1430
|
+
parts.push(`details: ${e.details}`);
|
|
1431
|
+
if (e.hint)
|
|
1432
|
+
parts.push(`hint: ${e.hint}`);
|
|
1433
|
+
message = parts.join(" ");
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
message = String(err);
|
|
1437
|
+
}
|
|
1438
|
+
const apiKeyHash = currentApiKeyHash();
|
|
1439
|
+
if (apiKeyHash) {
|
|
1440
|
+
void emitSignal({
|
|
1441
|
+
apiKeyHash,
|
|
1442
|
+
tool: name,
|
|
1443
|
+
action: "exception",
|
|
1444
|
+
severity: "action_needed",
|
|
1445
|
+
summary: `${name}: ${message}`.slice(0, 500),
|
|
1446
|
+
payload: { source: "mcp-server" },
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
626
1449
|
return {
|
|
627
1450
|
content: [{ type: "text", text: `Error: ${message}` }],
|
|
628
1451
|
isError: true,
|
|
@@ -635,7 +1458,7 @@ export async function startServer() {
|
|
|
635
1458
|
const server = createServer();
|
|
636
1459
|
const transport = new StdioServerTransport();
|
|
637
1460
|
await server.connect(transport);
|
|
638
|
-
// Server is running
|
|
1461
|
+
// Server is running -- errors go to stderr so they don't corrupt the MCP stream
|
|
639
1462
|
process.stderr.write("UnClick MCP server running on stdio\n");
|
|
640
1463
|
}
|
|
641
1464
|
//# sourceMappingURL=server.js.map
|