@intangle/mcp-server 2.5.4 → 2.5.6

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/.env.local CHANGED
@@ -1,2 +1,3 @@
1
1
  MCP_API_KEY=mcp_placeholder_key_will_be_replaced_by_user_specific_key
2
2
  NEXT_APP_URL=https://intangle.app
3
+ VERCEL_BYPASS_TOKEN=igXBygPO1T5dWLBaVyQiN9QB7IECZ50O
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { dirname, join } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
10
+ import { CallToolRequestSchema, ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { config } from "dotenv";
12
12
  import fetch from "node-fetch";
13
13
  import { TOOLS } from "./tool-definitions.js";
@@ -101,6 +101,13 @@ try {
101
101
  }
102
102
  return url.toString();
103
103
  }
104
+ function buildRemoteApiUrl() {
105
+ const url = new URL("/api/mcp-remote", API_BASE_URL);
106
+ if (VERCEL_BYPASS_TOKEN) {
107
+ url.searchParams.set("x-vercel-protection-bypass", VERCEL_BYPASS_TOKEN);
108
+ }
109
+ return url.toString();
110
+ }
104
111
  async function makeApiCall(endpoint, data, timeoutMs) {
105
112
  // Ensure we have client info before making requests
106
113
  ensureClientInfo();
@@ -160,6 +167,57 @@ try {
160
167
  clearTimeout(timeoutId);
161
168
  }
162
169
  }
170
+ async function makeRemoteRpcCall(method, params) {
171
+ ensureClientInfo();
172
+ const headers = {
173
+ "Content-Type": "application/json",
174
+ Authorization: `Bearer ${MCP_API_KEY}`,
175
+ "User-Agent": mcpClientName
176
+ ? `${mcpClientName}/${mcpClientVersion || "unknown"} (mcp-stdio)`
177
+ : "MCP-Client-Stdio/1.1.2 (mcp)",
178
+ };
179
+ if (mcpClientName) {
180
+ headers["X-MCP-Client-Name"] = mcpClientName;
181
+ }
182
+ if (mcpClientVersion) {
183
+ headers["X-MCP-Client-Version"] = mcpClientVersion;
184
+ }
185
+ if (VERCEL_BYPASS_TOKEN) {
186
+ headers["x-vercel-protection-bypass"] = VERCEL_BYPASS_TOKEN;
187
+ }
188
+ const requestId = Date.now();
189
+ const rpcBody = {
190
+ jsonrpc: "2.0",
191
+ id: requestId,
192
+ method,
193
+ params,
194
+ };
195
+ const response = await fetch(buildRemoteApiUrl(), {
196
+ method: "POST",
197
+ headers,
198
+ body: JSON.stringify(rpcBody),
199
+ });
200
+ const responseText = await response.text();
201
+ let responseJson = {};
202
+ try {
203
+ responseJson = responseText ? JSON.parse(responseText) : {};
204
+ }
205
+ catch {
206
+ throw new Error(`Failed to parse MCP remote ${method} response`);
207
+ }
208
+ if (!response.ok) {
209
+ const message = responseJson?.error?.message ||
210
+ responseJson?.message ||
211
+ `${response.status} ${response.statusText}`;
212
+ throw new Error(`MCP remote ${method} failed: ${message}`);
213
+ }
214
+ if (responseJson?.error) {
215
+ const code = responseJson.error.code;
216
+ const message = responseJson.error.message || "Unknown MCP remote error";
217
+ throw new Error(`MCP remote ${method} error (${code}): ${message}`);
218
+ }
219
+ return responseJson?.result ?? {};
220
+ }
163
221
  const server = new Server({
164
222
  name: "intangle-context",
165
223
  version: "1.0.0",
@@ -171,12 +229,33 @@ try {
171
229
  ],
172
230
  }, {
173
231
  capabilities: {
232
+ resources: {},
174
233
  tools: {},
175
234
  },
176
235
  });
177
236
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
178
237
  tools: TOOLS,
179
238
  }));
239
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
240
+ const result = await makeRemoteRpcCall("resources/list", {});
241
+ return {
242
+ resources: Array.isArray(result?.resources) ? result.resources : [],
243
+ nextCursor: typeof result?.nextCursor === "string" ? result.nextCursor : undefined,
244
+ };
245
+ });
246
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
247
+ const uri = request?.params?.uri;
248
+ if (!uri || typeof uri !== "string") {
249
+ throw new McpError(ErrorCode.InvalidParams, "uri is required for resources/read");
250
+ }
251
+ const result = await makeRemoteRpcCall("resources/read", { uri });
252
+ return {
253
+ contents: Array.isArray(result?.contents) ? result.contents : [],
254
+ };
255
+ });
256
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
257
+ resourceTemplates: [],
258
+ }));
180
259
  async function handleSearchContext(args) {
181
260
  const { space_id, query, topics } = args;
182
261
  // Require space_id
@@ -250,16 +329,39 @@ try {
250
329
  delete: args.delete,
251
330
  });
252
331
  }
332
+ async function handleUpdateFolders(args) {
333
+ if (!args.space_id) {
334
+ throw new Error("space_id is required. Use view_spaces to see available options.");
335
+ }
336
+ const hasOperation = !!args.list ||
337
+ (Array.isArray(args.create) && args.create.length > 0) ||
338
+ (Array.isArray(args.rename) && args.rename.length > 0) ||
339
+ (Array.isArray(args.move_items) && args.move_items.length > 0) ||
340
+ (Array.isArray(args.delete) && args.delete.length > 0);
341
+ if (!hasOperation) {
342
+ throw new Error("At least one operation is required: list, create, rename, move_items, or delete.");
343
+ }
344
+ return makeApiCall("update-folders", {
345
+ space_id: args.space_id,
346
+ list: args.list,
347
+ create: args.create,
348
+ rename: args.rename,
349
+ move_items: args.move_items,
350
+ delete: args.delete,
351
+ });
352
+ }
253
353
  async function handleMessage(args) {
254
354
  if (!args.space_id || !args.content) {
255
355
  throw new Error("space_id and content are required");
256
356
  }
357
+ // Keep one-turn MCP calls safely under common external tool-call deadlines
358
+ // while still allowing caller override for longer jobs.
257
359
  const timeoutMs = typeof args.timeout_ms === "number" && Number.isFinite(args.timeout_ms)
258
360
  ? Math.max(10_000, Math.min(300_000, Math.trunc(args.timeout_ms)))
259
- : undefined;
361
+ : 100_000;
260
362
  // Give backend a little extra headroom beyond requested assistant timeout.
261
- const requestTimeout = timeoutMs ? timeoutMs + 10_000 : undefined;
262
- return makeApiCall("message", args, requestTimeout);
363
+ const requestTimeout = timeoutMs + 10_000;
364
+ return makeApiCall("message", { ...args, timeout_ms: timeoutMs }, requestTimeout);
263
365
  }
264
366
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
265
367
  const { name, arguments: args } = request.params;
@@ -295,6 +397,9 @@ try {
295
397
  case "update_space":
296
398
  result = await handleUpdateSpace(args);
297
399
  break;
400
+ case "update_folders":
401
+ result = await handleUpdateFolders(args);
402
+ break;
298
403
  case "message":
299
404
  result = await handleMessage(args);
300
405
  break;
@@ -1,9 +1,9 @@
1
1
  // Tools definition - matches the stdio server
2
- export const TOOLS = [
2
+ const TOOL_DEFINITIONS = [
3
3
  {
4
4
  name: "search",
5
5
  title: "Search Space",
6
- description: "Search for context, tasks, skills, and projects within a space. System automatically extracts quantity from natural language ('show 3 tasks' → 3 results, 'the last one' → 1 result) and intelligently formats results (1-3 items → summaries, 4+ items → IDs only). Use fetch tool to get full content for specific IDs when needed.",
6
+ description: "Search for context, tasks, skills, and projects within a space. System automatically extracts quantity from natural language ('show 3 tasks' → 3 results, 'the last one' → 1 result) and intelligently formats results (1-3 items → summaries, 4+ items → IDs only). Default to one search per question, then use fetch_items for full details instead of repeating paraphrased searches.",
7
7
  inputSchema: {
8
8
  type: "object",
9
9
  properties: {
@@ -90,7 +90,7 @@ export const TOOLS = [
90
90
  {
91
91
  name: "start",
92
92
  title: "Start Space Session",
93
- description: "Begin working in a space. Returns a dynamic briefing including: recent developments, current priorities, active tasks, user preferences for this space, and learned insights about how the user/org works. Use this context to personalize your assistance and anticipate needs.",
93
+ description: "Load a space fast for a new external caller. Returns assistant-curated current items, useful insights, skills, recent conversations, and assistant preferences so the caller can get oriented quickly and fetch deeper details only when needed.",
94
94
  inputSchema: {
95
95
  type: "object",
96
96
  properties: {
@@ -157,7 +157,7 @@ export const TOOLS = [
157
157
  {
158
158
  name: "update_memory",
159
159
  title: "Update Memory",
160
- description: "Add, update, or delete items in a space. Supports any combination of operations in a single call.\n\nTIPS FOR RICH MEMORY: The more context you provide, the smarter the system becomes:\n- Include WHY something matters, not just WHAT it is\n- Note user preferences, patterns, and approaches you observe\n- Describe repeatable workflows as skills (step-by-step procedures)\n- Link related items using parent_id or linkedItemIds\n- Use descriptive titles that capture the essence\n- Add context about decisions, reasoning, and outcomes\n\nThe system learns from patterns - sharing how the user works helps it anticipate their needs.",
160
+ description: "Add, update, or delete items in a space. Supports any combination of operations in a single call.\n\nIMPORTANT: Include at least one operation: add, update, or delete.\n\nTIPS FOR RICH MEMORY: The more context you provide, the smarter the system becomes:\n- Include WHY something matters, not just WHAT it is\n- Note user preferences, patterns, and approaches you observe\n- Describe repeatable workflows as skills (step-by-step procedures)\n- Link related items using parent_id or linkedItemIds\n- Use descriptive titles that capture the essence\n- Add context about decisions, reasoning, and outcomes\n\nThe system learns from patterns - sharing how the user works helps it anticipate their needs.",
161
161
  inputSchema: {
162
162
  type: "object",
163
163
  properties: {
@@ -185,8 +185,8 @@ export const TOOLS = [
185
185
  },
186
186
  type: {
187
187
  type: "string",
188
- enum: ["task", "context", "skill", "document"],
189
- description: "REQUIRED: Item type. 'task' for actionable items, 'context' for knowledge/facts, 'skill' for workflows, 'document' for long-form reference material. Omitting this triggers expensive auto-classification."
188
+ enum: ["task", "context", "skill", "document", "insight"],
189
+ description: "REQUIRED: Item type. 'task' for actionable items, 'context' for knowledge/facts, 'skill' for workflows, 'document' for long-form reference material, 'insight' for durable patterns/principles. Omitting this triggers expensive auto-classification."
190
190
  },
191
191
  subtasks: {
192
192
  type: "array",
@@ -306,13 +306,117 @@ export const TOOLS = [
306
306
  }
307
307
  }
308
308
  },
309
- required: ["space_id"]
309
+ required: ["space_id"],
310
+ anyOf: [{ required: ["add"] }, { required: ["update"] }, { required: ["delete"] }]
311
+ }
312
+ },
313
+ {
314
+ name: "update_folders",
315
+ title: "Update Folders",
316
+ description: "Unified folder management. Supports list, create, rename, move_items, and delete operations in one call so external MCP callers can organize memory the same way as the in-app assistant.",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {
320
+ space_id: {
321
+ type: "string",
322
+ description: "REQUIRED: Space to operate in (use view_spaces to see available options)."
323
+ },
324
+ list: {
325
+ type: "object",
326
+ description: "Optional list operation. Use this first to discover existing folders and IDs.",
327
+ properties: {
328
+ section_type: {
329
+ type: "string",
330
+ enum: [
331
+ "projects",
332
+ "insights",
333
+ "insights_space",
334
+ "insights_user",
335
+ "agents",
336
+ "skills",
337
+ "conversations",
338
+ "dashboard"
339
+ ],
340
+ description: "Optional: filter folders by sidebar section."
341
+ }
342
+ }
343
+ },
344
+ create: {
345
+ type: "array",
346
+ description: "Optional create operations.",
347
+ items: {
348
+ type: "object",
349
+ properties: {
350
+ name: { type: "string", description: "Folder name." },
351
+ section_type: {
352
+ type: "string",
353
+ enum: [
354
+ "projects",
355
+ "insights",
356
+ "insights_space",
357
+ "insights_user",
358
+ "agents",
359
+ "skills",
360
+ "conversations",
361
+ "dashboard"
362
+ ],
363
+ description: "Sidebar section this folder belongs to."
364
+ },
365
+ parent_id: {
366
+ type: "string",
367
+ description: "Optional parent folder ID for nesting."
368
+ }
369
+ },
370
+ required: ["name", "section_type"]
371
+ }
372
+ },
373
+ rename: {
374
+ type: "array",
375
+ description: "Optional rename operations.",
376
+ items: {
377
+ type: "object",
378
+ properties: {
379
+ folder_id: { type: "string", description: "Folder ID to rename." },
380
+ new_name: { type: "string", description: "New folder name." }
381
+ },
382
+ required: ["folder_id", "new_name"]
383
+ }
384
+ },
385
+ move_items: {
386
+ type: "array",
387
+ description: "Optional item move operations. Set folder_id to null to move an item back to root.",
388
+ items: {
389
+ type: "object",
390
+ properties: {
391
+ item_id: { type: "string", description: "Item ID to move." },
392
+ folder_id: {
393
+ type: ["string", "null"],
394
+ description: "Target folder ID, or null to remove from folder."
395
+ }
396
+ },
397
+ required: ["item_id", "folder_id"]
398
+ }
399
+ },
400
+ delete: {
401
+ type: "array",
402
+ description: "Optional delete operations (folder IDs).",
403
+ items: { type: "string" }
404
+ }
405
+ },
406
+ required: ["space_id"],
407
+ anyOf: [
408
+ { required: ["list"] },
409
+ { required: ["create"] },
410
+ { required: ["rename"] },
411
+ { required: ["move_items"] },
412
+ { required: ["delete"] }
413
+ ]
310
414
  }
311
415
  },
312
416
  {
313
417
  name: "message",
314
418
  title: "Message Assistant",
315
- description: "Send a user message to the Intangle assistant and wait for one assistant turn to complete. Returns assistant text, stop reason, session_id, and conversation_id for continuity.",
419
+ description: "Primary orchestration tool. Send a request to the Intangle assistant and wait for one assistant turn to complete. Returns assistant text plus continuity IDs (`session_id`, `conversation_id`) so callers can continue the same thread reliably across turns.",
316
420
  inputSchema: {
317
421
  type: "object",
318
422
  properties: {
@@ -330,11 +434,11 @@ export const TOOLS = [
330
434
  },
331
435
  conversation_id: {
332
436
  type: "string",
333
- description: "Optional existing conversation/chat summary ID to resume continuity."
437
+ description: "Optional existing conversation/chat summary ID to resume a specific conversation. If omitted, Intangle uses/creates the active conversation for this space."
334
438
  },
335
439
  session_id: {
336
440
  type: "string",
337
- description: "Optional runtime session ID to reuse across multiple message calls."
441
+ description: "Optional runtime session ID to reuse across calls. Reuse the returned session_id for best continuity and lower warm-up overhead."
338
442
  },
339
443
  project_id: {
340
444
  type: "string",
@@ -355,9 +459,9 @@ export const TOOLS = [
355
459
  },
356
460
  required: ["space_id", "content"]
357
461
  }
358
- },
462
+ }
359
463
  // DISABLED: memory_action tool is broken and causing errors.
360
- // Pending OHM protocol fix. Use update_space, search, or fetch_items instead.
464
+ // Pending OHM protocol fix. Use update_memory, search, or fetch_items instead.
361
465
  // {
362
466
  // name: "memory_action",
363
467
  // title: "Memory Action (OHM Protocol)",
@@ -380,3 +484,31 @@ export const TOOLS = [
380
484
  // }
381
485
  // }
382
486
  ];
487
+ const READ_ONLY_TOOLS = new Set([
488
+ "search",
489
+ "fetch_items",
490
+ "start",
491
+ "view_spaces",
492
+ "view_space"
493
+ ]);
494
+ const DESTRUCTIVE_TOOLS = new Set([
495
+ "create_space",
496
+ "update_memory",
497
+ "update_folders"
498
+ ]);
499
+ const IDEMPOTENT_TOOLS = new Set([
500
+ "search",
501
+ "fetch_items",
502
+ "start",
503
+ "view_spaces",
504
+ "view_space"
505
+ ]);
506
+ export const TOOLS = TOOL_DEFINITIONS.map((tool) => ({
507
+ ...tool,
508
+ annotations: {
509
+ ...(READ_ONLY_TOOLS.has(tool.name) ? { readOnlyHint: true } : {}),
510
+ ...(DESTRUCTIVE_TOOLS.has(tool.name) ? { destructiveHint: true } : {}),
511
+ ...(IDEMPOTENT_TOOLS.has(tool.name) ? { idempotentHint: true } : {}),
512
+ openWorldHint: true
513
+ }
514
+ }));
package/index.ts CHANGED
@@ -12,8 +12,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
12
12
  import {
13
13
  CallToolRequestSchema,
14
14
  ErrorCode,
15
+ ListResourceTemplatesRequestSchema,
16
+ ListResourcesRequestSchema,
15
17
  ListToolsRequestSchema,
16
18
  McpError,
19
+ ReadResourceRequestSchema,
17
20
  } from "@modelcontextprotocol/sdk/types.js"
18
21
  import { config } from "dotenv"
19
22
  import fetch from "node-fetch"
@@ -134,6 +137,16 @@ try {
134
137
  return url.toString()
135
138
  }
136
139
 
140
+ function buildRemoteApiUrl() {
141
+ const url = new URL("/api/mcp-remote", API_BASE_URL)
142
+
143
+ if (VERCEL_BYPASS_TOKEN) {
144
+ url.searchParams.set("x-vercel-protection-bypass", VERCEL_BYPASS_TOKEN)
145
+ }
146
+
147
+ return url.toString()
148
+ }
149
+
137
150
  async function makeApiCall(endpoint: string, data: any, timeoutMs?: number) {
138
151
  // Ensure we have client info before making requests
139
152
  ensureClientInfo()
@@ -187,7 +200,9 @@ try {
187
200
  )
188
201
  }
189
202
 
190
- throw new Error(`API call failed: ${response.status} ${response.statusText}`)
203
+ throw new Error(
204
+ `API call failed: ${response.status} ${response.statusText}`
205
+ )
191
206
  }
192
207
 
193
208
  if (responseBody.trim() === "") {
@@ -207,6 +222,70 @@ try {
207
222
  }
208
223
  }
209
224
 
225
+ async function makeRemoteRpcCall(
226
+ method: string,
227
+ params: Record<string, unknown>
228
+ ) {
229
+ ensureClientInfo()
230
+
231
+ const headers: Record<string, string> = {
232
+ "Content-Type": "application/json",
233
+ Authorization: `Bearer ${MCP_API_KEY}`,
234
+ "User-Agent": mcpClientName
235
+ ? `${mcpClientName}/${mcpClientVersion || "unknown"} (mcp-stdio)`
236
+ : "MCP-Client-Stdio/1.1.2 (mcp)",
237
+ }
238
+
239
+ if (mcpClientName) {
240
+ headers["X-MCP-Client-Name"] = mcpClientName
241
+ }
242
+ if (mcpClientVersion) {
243
+ headers["X-MCP-Client-Version"] = mcpClientVersion
244
+ }
245
+ if (VERCEL_BYPASS_TOKEN) {
246
+ headers["x-vercel-protection-bypass"] = VERCEL_BYPASS_TOKEN
247
+ }
248
+
249
+ const requestId = Date.now()
250
+ const rpcBody = {
251
+ jsonrpc: "2.0",
252
+ id: requestId,
253
+ method,
254
+ params,
255
+ }
256
+
257
+ const response = await fetch(buildRemoteApiUrl(), {
258
+ method: "POST",
259
+ headers,
260
+ body: JSON.stringify(rpcBody),
261
+ })
262
+
263
+ const responseText = await response.text()
264
+ let responseJson: any = {}
265
+
266
+ try {
267
+ responseJson = responseText ? JSON.parse(responseText) : {}
268
+ } catch {
269
+ throw new Error(`Failed to parse MCP remote ${method} response`)
270
+ }
271
+
272
+ if (!response.ok) {
273
+ const message =
274
+ responseJson?.error?.message ||
275
+ responseJson?.message ||
276
+ `${response.status} ${response.statusText}`
277
+ throw new Error(`MCP remote ${method} failed: ${message}`)
278
+ }
279
+
280
+ if (responseJson?.error) {
281
+ const code = responseJson.error.code
282
+ const message = responseJson.error.message || "Unknown MCP remote error"
283
+ throw new Error(`MCP remote ${method} error (${code}): ${message}`)
284
+ }
285
+
286
+ return responseJson?.result ?? {}
287
+ }
288
+
210
289
  const server = new Server(
211
290
  {
212
291
  name: "intangle-context",
@@ -220,6 +299,7 @@ try {
220
299
  },
221
300
  {
222
301
  capabilities: {
302
+ resources: {},
223
303
  tools: {},
224
304
  },
225
305
  }
@@ -229,6 +309,34 @@ try {
229
309
  tools: TOOLS,
230
310
  }))
231
311
 
312
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
313
+ const result = await makeRemoteRpcCall("resources/list", {})
314
+ return {
315
+ resources: Array.isArray(result?.resources) ? result.resources : [],
316
+ nextCursor:
317
+ typeof result?.nextCursor === "string" ? result.nextCursor : undefined,
318
+ }
319
+ })
320
+
321
+ server.setRequestHandler(ReadResourceRequestSchema, async (request: any) => {
322
+ const uri = request?.params?.uri
323
+ if (!uri || typeof uri !== "string") {
324
+ throw new McpError(
325
+ ErrorCode.InvalidParams,
326
+ "uri is required for resources/read"
327
+ )
328
+ }
329
+
330
+ const result = await makeRemoteRpcCall("resources/read", { uri })
331
+ return {
332
+ contents: Array.isArray(result?.contents) ? result.contents : [],
333
+ }
334
+ })
335
+
336
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
337
+ resourceTemplates: [],
338
+ }))
339
+
232
340
  async function handleSearchContext(args: any) {
233
341
  const { space_id, query, topics } = args as {
234
342
  space_id: string
@@ -278,11 +386,15 @@ try {
278
386
  }
279
387
 
280
388
  if (!project_id && !slug) {
281
- throw new Error("Either project_id or slug (with space_id) is required. Use view_projects to get valid IDs.")
389
+ throw new Error(
390
+ "Either project_id or slug (with space_id) is required. Use view_projects to get valid IDs."
391
+ )
282
392
  }
283
393
 
284
394
  if (slug && !space_id) {
285
- throw new Error("space_id is required when using slug. Use view_spaces to see available options.")
395
+ throw new Error(
396
+ "space_id is required when using slug. Use view_spaces to see available options."
397
+ )
286
398
  }
287
399
 
288
400
  return makeApiCall("view-project", { project_id, space_id, slug })
@@ -310,9 +422,7 @@ try {
310
422
  }
311
423
 
312
424
  if (!args.add && !args.update && !args.delete) {
313
- throw new Error(
314
- "At least one operation must be provided"
315
- )
425
+ throw new Error("At least one operation must be provided")
316
426
  }
317
427
 
318
428
  // Ensure all add items have a type field to prevent classification timeout
@@ -320,7 +430,9 @@ try {
320
430
  if (add?.items && Array.isArray(add.items)) {
321
431
  for (const item of add.items) {
322
432
  if (!item.type) {
323
- log(`WARNING: Item "${item.title}" missing type field, defaulting to "context" to prevent classification timeout`)
433
+ log(
434
+ `WARNING: Item "${item.title}" missing type field, defaulting to "context" to prevent classification timeout`
435
+ )
324
436
  item.type = "context"
325
437
  }
326
438
  }
@@ -336,20 +448,56 @@ try {
336
448
  })
337
449
  }
338
450
 
451
+ async function handleUpdateFolders(args: any) {
452
+ if (!args.space_id) {
453
+ throw new Error(
454
+ "space_id is required. Use view_spaces to see available options."
455
+ )
456
+ }
457
+
458
+ const hasOperation =
459
+ !!args.list ||
460
+ (Array.isArray(args.create) && args.create.length > 0) ||
461
+ (Array.isArray(args.rename) && args.rename.length > 0) ||
462
+ (Array.isArray(args.move_items) && args.move_items.length > 0) ||
463
+ (Array.isArray(args.delete) && args.delete.length > 0)
464
+
465
+ if (!hasOperation) {
466
+ throw new Error(
467
+ "At least one operation is required: list, create, rename, move_items, or delete."
468
+ )
469
+ }
470
+
471
+ return makeApiCall("update-folders", {
472
+ space_id: args.space_id,
473
+ list: args.list,
474
+ create: args.create,
475
+ rename: args.rename,
476
+ move_items: args.move_items,
477
+ delete: args.delete,
478
+ })
479
+ }
480
+
339
481
  async function handleMessage(args: any) {
340
482
  if (!args.space_id || !args.content) {
341
483
  throw new Error("space_id and content are required")
342
484
  }
343
485
 
486
+ // Keep one-turn MCP calls safely under common external tool-call deadlines
487
+ // while still allowing caller override for longer jobs.
344
488
  const timeoutMs =
345
489
  typeof args.timeout_ms === "number" && Number.isFinite(args.timeout_ms)
346
490
  ? Math.max(10_000, Math.min(300_000, Math.trunc(args.timeout_ms)))
347
- : undefined
491
+ : 100_000
348
492
 
349
493
  // Give backend a little extra headroom beyond requested assistant timeout.
350
- const requestTimeout = timeoutMs ? timeoutMs + 10_000 : undefined
494
+ const requestTimeout = timeoutMs + 10_000
351
495
 
352
- return makeApiCall("message", args, requestTimeout)
496
+ return makeApiCall(
497
+ "message",
498
+ { ...args, timeout_ms: timeoutMs },
499
+ requestTimeout
500
+ )
353
501
  }
354
502
 
355
503
  server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
@@ -388,6 +536,9 @@ try {
388
536
  case "update_space":
389
537
  result = await handleUpdateSpace(args)
390
538
  break
539
+ case "update_folders":
540
+ result = await handleUpdateFolders(args)
541
+ break
391
542
  case "message":
392
543
  result = await handleMessage(args)
393
544
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intangle/mcp-server",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "description": "Model Context Protocol server for Intangle - AI context that persists across conversations",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,10 +1,10 @@
1
1
  // Tools definition - matches the stdio server
2
- export const TOOLS = [
2
+ const TOOL_DEFINITIONS = [
3
3
  {
4
4
  name: "search",
5
5
  title: "Search Space",
6
6
  description:
7
- "Search for context, tasks, skills, and projects within a space. System automatically extracts quantity from natural language ('show 3 tasks' → 3 results, 'the last one' → 1 result) and intelligently formats results (1-3 items → summaries, 4+ items → IDs only). Use fetch tool to get full content for specific IDs when needed.",
7
+ "Search for context, tasks, skills, and projects within a space. System automatically extracts quantity from natural language ('show 3 tasks' → 3 results, 'the last one' → 1 result) and intelligently formats results (1-3 items → summaries, 4+ items → IDs only). Default to one search per question, then use fetch_items for full details instead of repeating paraphrased searches.",
8
8
  inputSchema: {
9
9
  type: "object",
10
10
  properties: {
@@ -97,7 +97,7 @@ export const TOOLS = [
97
97
  name: "start",
98
98
  title: "Start Space Session",
99
99
  description:
100
- "Begin working in a space. Returns a dynamic briefing including: recent developments, current priorities, active tasks, user preferences for this space, and learned insights about how the user/org works. Use this context to personalize your assistance and anticipate needs.",
100
+ "Load a space fast for a new external caller. Returns assistant-curated current items, useful insights, skills, recent conversations, and assistant preferences so the caller can get oriented quickly and fetch deeper details only when needed.",
101
101
  inputSchema: {
102
102
  type: "object",
103
103
  properties: {
@@ -171,7 +171,7 @@ export const TOOLS = [
171
171
  name: "update_memory",
172
172
  title: "Update Memory",
173
173
  description:
174
- "Add, update, or delete items in a space. Supports any combination of operations in a single call.\n\nTIPS FOR RICH MEMORY: The more context you provide, the smarter the system becomes:\n- Include WHY something matters, not just WHAT it is\n- Note user preferences, patterns, and approaches you observe\n- Describe repeatable workflows as skills (step-by-step procedures)\n- Link related items using parent_id or linkedItemIds\n- Use descriptive titles that capture the essence\n- Add context about decisions, reasoning, and outcomes\n\nThe system learns from patterns - sharing how the user works helps it anticipate their needs.",
174
+ "Add, update, or delete items in a space. Supports any combination of operations in a single call.\n\nIMPORTANT: Include at least one operation: add, update, or delete.\n\nTIPS FOR RICH MEMORY: The more context you provide, the smarter the system becomes:\n- Include WHY something matters, not just WHAT it is\n- Note user preferences, patterns, and approaches you observe\n- Describe repeatable workflows as skills (step-by-step procedures)\n- Link related items using parent_id or linkedItemIds\n- Use descriptive titles that capture the essence\n- Add context about decisions, reasoning, and outcomes\n\nThe system learns from patterns - sharing how the user works helps it anticipate their needs.",
175
175
  inputSchema: {
176
176
  type: "object",
177
177
  properties: {
@@ -203,9 +203,9 @@ export const TOOLS = [
203
203
  },
204
204
  type: {
205
205
  type: "string",
206
- enum: ["task", "context", "skill", "document"],
206
+ enum: ["task", "context", "skill", "document", "insight"],
207
207
  description:
208
- "REQUIRED: Item type. 'task' for actionable items, 'context' for knowledge/facts, 'skill' for workflows, 'document' for long-form reference material. Omitting this triggers expensive auto-classification."
208
+ "REQUIRED: Item type. 'task' for actionable items, 'context' for knowledge/facts, 'skill' for workflows, 'document' for long-form reference material, 'insight' for durable patterns/principles. Omitting this triggers expensive auto-classification."
209
209
  },
210
210
  subtasks: {
211
211
  type: "array",
@@ -339,14 +339,122 @@ export const TOOLS = [
339
339
  }
340
340
  }
341
341
  },
342
- required: ["space_id"]
342
+ required: ["space_id"],
343
+ anyOf: [{ required: ["add"] }, { required: ["update"] }, { required: ["delete"] }]
344
+ }
345
+ },
346
+ {
347
+ name: "update_folders",
348
+ title: "Update Folders",
349
+ description:
350
+ "Unified folder management. Supports list, create, rename, move_items, and delete operations in one call so external MCP callers can organize memory the same way as the in-app assistant.",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ space_id: {
355
+ type: "string",
356
+ description:
357
+ "REQUIRED: Space to operate in (use view_spaces to see available options)."
358
+ },
359
+ list: {
360
+ type: "object",
361
+ description:
362
+ "Optional list operation. Use this first to discover existing folders and IDs.",
363
+ properties: {
364
+ section_type: {
365
+ type: "string",
366
+ enum: [
367
+ "projects",
368
+ "insights",
369
+ "insights_space",
370
+ "insights_user",
371
+ "agents",
372
+ "skills",
373
+ "conversations",
374
+ "dashboard"
375
+ ],
376
+ description: "Optional: filter folders by sidebar section."
377
+ }
378
+ }
379
+ },
380
+ create: {
381
+ type: "array",
382
+ description: "Optional create operations.",
383
+ items: {
384
+ type: "object",
385
+ properties: {
386
+ name: { type: "string", description: "Folder name." },
387
+ section_type: {
388
+ type: "string",
389
+ enum: [
390
+ "projects",
391
+ "insights",
392
+ "insights_space",
393
+ "insights_user",
394
+ "agents",
395
+ "skills",
396
+ "conversations",
397
+ "dashboard"
398
+ ],
399
+ description: "Sidebar section this folder belongs to."
400
+ },
401
+ parent_id: {
402
+ type: "string",
403
+ description: "Optional parent folder ID for nesting."
404
+ }
405
+ },
406
+ required: ["name", "section_type"]
407
+ }
408
+ },
409
+ rename: {
410
+ type: "array",
411
+ description: "Optional rename operations.",
412
+ items: {
413
+ type: "object",
414
+ properties: {
415
+ folder_id: { type: "string", description: "Folder ID to rename." },
416
+ new_name: { type: "string", description: "New folder name." }
417
+ },
418
+ required: ["folder_id", "new_name"]
419
+ }
420
+ },
421
+ move_items: {
422
+ type: "array",
423
+ description:
424
+ "Optional item move operations. Set folder_id to null to move an item back to root.",
425
+ items: {
426
+ type: "object",
427
+ properties: {
428
+ item_id: { type: "string", description: "Item ID to move." },
429
+ folder_id: {
430
+ type: ["string", "null"],
431
+ description: "Target folder ID, or null to remove from folder."
432
+ }
433
+ },
434
+ required: ["item_id", "folder_id"]
435
+ }
436
+ },
437
+ delete: {
438
+ type: "array",
439
+ description: "Optional delete operations (folder IDs).",
440
+ items: { type: "string" }
441
+ }
442
+ },
443
+ required: ["space_id"],
444
+ anyOf: [
445
+ { required: ["list"] },
446
+ { required: ["create"] },
447
+ { required: ["rename"] },
448
+ { required: ["move_items"] },
449
+ { required: ["delete"] }
450
+ ]
343
451
  }
344
452
  },
345
453
  {
346
454
  name: "message",
347
455
  title: "Message Assistant",
348
456
  description:
349
- "Send a user message to the Intangle assistant and wait for one assistant turn to complete. Returns assistant text, stop reason, session_id, and conversation_id for continuity.",
457
+ "Primary orchestration tool. Send a request to the Intangle assistant and wait for one assistant turn to complete. Returns assistant text plus continuity IDs (`session_id`, `conversation_id`) so callers can continue the same thread reliably across turns.",
350
458
  inputSchema: {
351
459
  type: "object",
352
460
  properties: {
@@ -366,12 +474,12 @@ export const TOOLS = [
366
474
  conversation_id: {
367
475
  type: "string",
368
476
  description:
369
- "Optional existing conversation/chat summary ID to resume continuity."
477
+ "Optional existing conversation/chat summary ID to resume a specific conversation. If omitted, Intangle uses/creates the active conversation for this space."
370
478
  },
371
479
  session_id: {
372
480
  type: "string",
373
481
  description:
374
- "Optional runtime session ID to reuse across multiple message calls."
482
+ "Optional runtime session ID to reuse across calls. Reuse the returned session_id for best continuity and lower warm-up overhead."
375
483
  },
376
484
  project_id: {
377
485
  type: "string",
@@ -394,9 +502,9 @@ export const TOOLS = [
394
502
  },
395
503
  required: ["space_id", "content"]
396
504
  }
397
- },
505
+ }
398
506
  // DISABLED: memory_action tool is broken and causing errors.
399
- // Pending OHM protocol fix. Use update_space, search, or fetch_items instead.
507
+ // Pending OHM protocol fix. Use update_memory, search, or fetch_items instead.
400
508
  // {
401
509
  // name: "memory_action",
402
510
  // title: "Memory Action (OHM Protocol)",
@@ -419,3 +527,35 @@ export const TOOLS = [
419
527
  // }
420
528
  // }
421
529
  ]
530
+
531
+ const READ_ONLY_TOOLS = new Set([
532
+ "search",
533
+ "fetch_items",
534
+ "start",
535
+ "view_spaces",
536
+ "view_space"
537
+ ])
538
+
539
+ const DESTRUCTIVE_TOOLS = new Set([
540
+ "create_space",
541
+ "update_memory",
542
+ "update_folders"
543
+ ])
544
+
545
+ const IDEMPOTENT_TOOLS = new Set([
546
+ "search",
547
+ "fetch_items",
548
+ "start",
549
+ "view_spaces",
550
+ "view_space"
551
+ ])
552
+
553
+ export const TOOLS = TOOL_DEFINITIONS.map((tool) => ({
554
+ ...tool,
555
+ annotations: {
556
+ ...(READ_ONLY_TOOLS.has(tool.name) ? { readOnlyHint: true } : {}),
557
+ ...(DESTRUCTIVE_TOOLS.has(tool.name) ? { destructiveHint: true } : {}),
558
+ ...(IDEMPOTENT_TOOLS.has(tool.name) ? { idempotentHint: true } : {}),
559
+ openWorldHint: true
560
+ }
561
+ }))