@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.
Files changed (142) hide show
  1. package/README.md +160 -139
  2. package/dist/abn-tool.js +1 -1
  3. package/dist/bgg-tool.js +1 -1
  4. package/dist/carboninterface-tool.js +1 -1
  5. package/dist/cards/card.d.ts +9 -0
  6. package/dist/cards/card.d.ts.map +1 -0
  7. package/dist/cards/card.js +4 -0
  8. package/dist/cards/card.js.map +1 -0
  9. package/dist/cards/search-memory-card.d.ts +11 -0
  10. package/dist/cards/search-memory-card.d.ts.map +1 -0
  11. package/dist/cards/search-memory-card.js +75 -0
  12. package/dist/cards/search-memory-card.js.map +1 -0
  13. package/dist/cards/search-memory-card.test.d.ts +2 -0
  14. package/dist/cards/search-memory-card.test.d.ts.map +1 -0
  15. package/dist/cards/search-memory-card.test.js +59 -0
  16. package/dist/cards/search-memory-card.test.js.map +1 -0
  17. package/dist/catalog.d.ts.map +1 -1
  18. package/dist/catalog.js +265 -4
  19. package/dist/catalog.js.map +1 -1
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +96 -6
  22. package/dist/client.js.map +1 -1
  23. package/dist/converter-tools.js +1 -1
  24. package/dist/crews-tool.d.ts +12 -0
  25. package/dist/crews-tool.d.ts.map +1 -0
  26. package/dist/crews-tool.js +125 -0
  27. package/dist/crews-tool.js.map +1 -0
  28. package/dist/gdelt-tool.js +4 -4
  29. package/dist/hackernews-tool.js +1 -1
  30. package/dist/index.js +0 -0
  31. package/dist/keychain-secure-input.js +42 -42
  32. package/dist/line-tool.js +1 -1
  33. package/dist/linear-tool.js +73 -73
  34. package/dist/local-catalog-handlers.js +1 -1
  35. package/dist/local-catalog-handlers.js.map +1 -1
  36. package/dist/local-tools.js +7 -7
  37. package/dist/local-tools.js.map +1 -1
  38. package/dist/memory/__tests__/bitemporal.test.d.ts +8 -0
  39. package/dist/memory/__tests__/bitemporal.test.d.ts.map +1 -0
  40. package/dist/memory/__tests__/bitemporal.test.js +148 -0
  41. package/dist/memory/__tests__/bitemporal.test.js.map +1 -0
  42. package/dist/memory/__tests__/hybrid-search.test.d.ts +14 -0
  43. package/dist/memory/__tests__/hybrid-search.test.d.ts.map +1 -0
  44. package/dist/memory/__tests__/hybrid-search.test.js +304 -0
  45. package/dist/memory/__tests__/hybrid-search.test.js.map +1 -0
  46. package/dist/memory/agent.d.ts +34 -0
  47. package/dist/memory/agent.d.ts.map +1 -0
  48. package/dist/memory/agent.js +69 -0
  49. package/dist/memory/agent.js.map +1 -0
  50. package/dist/memory/conflicts.d.ts +48 -0
  51. package/dist/memory/conflicts.d.ts.map +1 -0
  52. package/dist/memory/conflicts.js +209 -0
  53. package/dist/memory/conflicts.js.map +1 -0
  54. package/dist/memory/db.d.ts +25 -0
  55. package/dist/memory/db.d.ts.map +1 -0
  56. package/dist/memory/db.js +144 -0
  57. package/dist/memory/db.js.map +1 -0
  58. package/dist/memory/device.d.ts +20 -0
  59. package/dist/memory/device.d.ts.map +1 -0
  60. package/dist/memory/device.js +48 -0
  61. package/dist/memory/device.js.map +1 -0
  62. package/dist/memory/embeddings.d.ts +10 -0
  63. package/dist/memory/embeddings.d.ts.map +1 -0
  64. package/dist/memory/embeddings.js +40 -0
  65. package/dist/memory/embeddings.js.map +1 -0
  66. package/dist/memory/handlers.d.ts +11 -0
  67. package/dist/memory/handlers.d.ts.map +1 -0
  68. package/dist/memory/handlers.js +219 -0
  69. package/dist/memory/handlers.js.map +1 -0
  70. package/dist/memory/instrumentation.d.ts +38 -0
  71. package/dist/memory/instrumentation.d.ts.map +1 -0
  72. package/dist/memory/instrumentation.js +97 -0
  73. package/dist/memory/instrumentation.js.map +1 -0
  74. package/dist/memory/load-events.d.ts +18 -0
  75. package/dist/memory/load-events.d.ts.map +1 -0
  76. package/dist/memory/load-events.js +61 -0
  77. package/dist/memory/load-events.js.map +1 -0
  78. package/dist/memory/local.d.ts +40 -0
  79. package/dist/memory/local.d.ts.map +1 -0
  80. package/dist/memory/local.js +400 -0
  81. package/dist/memory/local.js.map +1 -0
  82. package/dist/memory/session-state.d.ts +37 -0
  83. package/dist/memory/session-state.d.ts.map +1 -0
  84. package/dist/memory/session-state.js +82 -0
  85. package/dist/memory/session-state.js.map +1 -0
  86. package/dist/memory/supabase.d.ts +104 -0
  87. package/dist/memory/supabase.d.ts.map +1 -0
  88. package/dist/memory/supabase.js +710 -0
  89. package/dist/memory/supabase.js.map +1 -0
  90. package/dist/memory/tenant-settings.d.ts +33 -0
  91. package/dist/memory/tenant-settings.d.ts.map +1 -0
  92. package/dist/memory/tenant-settings.js +79 -0
  93. package/dist/memory/tenant-settings.js.map +1 -0
  94. package/dist/memory/tool-awareness.d.ts +66 -0
  95. package/dist/memory/tool-awareness.d.ts.map +1 -0
  96. package/dist/memory/tool-awareness.js +307 -0
  97. package/dist/memory/tool-awareness.js.map +1 -0
  98. package/dist/memory/types.d.ts +97 -0
  99. package/dist/memory/types.d.ts.map +1 -0
  100. package/dist/memory/types.js +5 -0
  101. package/dist/memory/types.js.map +1 -0
  102. package/dist/monday-tool.js +46 -46
  103. package/dist/musicbrainz-tool.js +1 -1
  104. package/dist/musicbrainz-tool.js.map +1 -1
  105. package/dist/numbers-tool.js +2 -2
  106. package/dist/openfoodfacts-tool.js +1 -1
  107. package/dist/openmeteo-tool.js +1 -1
  108. package/dist/radiobrowser-tool.js +2 -2
  109. package/dist/server.d.ts.map +1 -1
  110. package/dist/server.js +838 -15
  111. package/dist/server.js.map +1 -1
  112. package/dist/signals/emit.d.ts +11 -0
  113. package/dist/signals/emit.d.ts.map +1 -0
  114. package/dist/signals/emit.js +26 -0
  115. package/dist/signals/emit.js.map +1 -0
  116. package/dist/testpass-tool.d.ts +12 -0
  117. package/dist/testpass-tool.d.ts.map +1 -0
  118. package/dist/testpass-tool.js +121 -0
  119. package/dist/testpass-tool.js.map +1 -0
  120. package/dist/toilets-tool.js +2 -2
  121. package/dist/tool-wiring.d.ts +320 -4
  122. package/dist/tool-wiring.d.ts.map +1 -1
  123. package/dist/tool-wiring.js +246 -5
  124. package/dist/tool-wiring.js.map +1 -1
  125. package/dist/trivia-tool.js +5 -5
  126. package/dist/usgs-tool.js +1 -1
  127. package/dist/uxpass-tool.d.ts +24 -0
  128. package/dist/uxpass-tool.d.ts.map +1 -0
  129. package/dist/uxpass-tool.js +165 -0
  130. package/dist/uxpass-tool.js.map +1 -0
  131. package/dist/vault-bridge.js +7 -7
  132. package/dist/vercel-tool.d.ts +3 -0
  133. package/dist/vercel-tool.d.ts.map +1 -1
  134. package/dist/vercel-tool.js +198 -7
  135. package/dist/vercel-tool.js.map +1 -1
  136. package/dist/web-tools.d.ts +62 -0
  137. package/dist/web-tools.d.ts.map +1 -0
  138. package/dist/web-tools.js +271 -0
  139. package/dist/web-tools.js.map +1 -0
  140. package/package.json +69 -65
  141. package/public/icon.svg +15 -15
  142. 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
- const META_TOOLS = [
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' returns the image tool with its endpoints.",
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 describe what you want to do",
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 get decimal, binary, hex, and type (private/loopback/multicast).",
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: "UnClick",
448
- version: "0.1.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
- capabilities: { tools: {} },
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 expose only the 4 meta tools; individual tools remain callable
462
- // via unclick_call for backwards compat but aren't advertised to reduce noise.
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: [...META_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}\`) ${tool.description}`);
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}\` ${ep.name}`);
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
- const message = err instanceof Error ? err.message : String(err);
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 errors go to stderr so they don't corrupt the MCP stream
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