@intangle/mcp-server 2.6.18 → 2.6.20

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 CHANGED
@@ -51,6 +51,42 @@ Standard MCP (Claude desktop, Cursor): by adding to your `claude_desktop_config.
51
51
 
52
52
  3. **Restart Claude Desktop** to load the MCP server
53
53
 
54
+ ## Claude Code live Tan channel (opt-in preview)
55
+
56
+ The MCP server can forward new Tan-addressed messages from one explicitly bound
57
+ Intangle Room into an already-running Claude Code session and send replies back
58
+ through `intangle_channel_reply`. It does not attach to a session that was
59
+ started before the channel was enabled, and it does not replay older messages.
60
+
61
+ This custom channel requires Node.js 21 or later. The standard Intangle MCP
62
+ tools continue to support Node.js 18. Claude Code currently treats custom
63
+ channels as development channels; its channels feature is a research preview.
64
+
65
+ Configure the Intangle MCP server with the exact Space, conversation, and
66
+ participant IDs to receive Tan's addressed messages:
67
+
68
+ ```bash
69
+ claude mcp add intangle -s user \
70
+ --env MCP_API_KEY=<your-intangle-api-key> \
71
+ --env NEXT_APP_URL=https://renvoi.co/intangle \
72
+ --env INTANGLE_CLAUDE_CHANNEL_ENABLED=1 \
73
+ --env INTANGLE_CLAUDE_CHANNEL_SPACE_ID=<space-id> \
74
+ --env INTANGLE_CLAUDE_CHANNEL_CONVERSATION_ID=<conversation-id> \
75
+ --env INTANGLE_CLAUDE_CHANNEL_PARTICIPANT_ID=<agent-participant-id> \
76
+ -- npx -y @intangle/mcp-server
77
+ ```
78
+
79
+ Then start a new Claude Code session with the custom channel explicitly enabled:
80
+
81
+ ```bash
82
+ claude --dangerously-load-development-channels server:intangle
83
+ ```
84
+
85
+ Claude Code shows a development-channel warning and may ask you to trust the
86
+ MCP server. Messages arrive only while that session is open. Remove the three
87
+ `INTANGLE_CLAUDE_CHANNEL_*` scope values or omit the enabled flag to return to
88
+ ordinary MCP-only behavior.
89
+
54
90
  ## Usage
55
91
 
56
92
  Once configured, Claude will automatically have access to your Intangle context. You can:
@@ -0,0 +1,359 @@
1
+ export const CLAUDE_CHANNEL_REPLY_TOOL = "intangle_channel_reply";
2
+ export const CLAUDE_CHANNEL_ENABLED_ENV = "INTANGLE_CLAUDE_CHANNEL_ENABLED";
3
+ export const CLAUDE_CHANNEL_RUNTIME_ERROR = "Intangle Claude channels require Node.js 21 or later for the built-in WebSocket API; standard MCP tools still support Node.js 18.";
4
+ const CHANNEL_RECONNECT_SECONDS = 25;
5
+ const REPLAY_OVERLAP_MS = 5_000;
6
+ export function claudeChannelConfigFromEnv(env) {
7
+ if (env[CLAUDE_CHANNEL_ENABLED_ENV] !== "1")
8
+ return null;
9
+ const spaceId = nonblank(env.INTANGLE_CLAUDE_CHANNEL_SPACE_ID);
10
+ const conversationId = nonblank(env.INTANGLE_CLAUDE_CHANNEL_CONVERSATION_ID);
11
+ const participantId = nonblank(env.INTANGLE_CLAUDE_CHANNEL_PARTICIPANT_ID);
12
+ if (!spaceId || !conversationId || !participantId) {
13
+ throw new Error(`${CLAUDE_CHANNEL_ENABLED_ENV}=1 requires explicit Space, conversation, and participant IDs`);
14
+ }
15
+ return {
16
+ spaceId,
17
+ conversationId,
18
+ participantId,
19
+ participantLabel: nonblank(env.INTANGLE_CLAUDE_CHANNEL_PARTICIPANT_LABEL) || "Claude Code",
20
+ participantType: nonblank(env.INTANGLE_CLAUDE_CHANNEL_PARTICIPANT_TYPE) || "agent",
21
+ organizationId: nonblank(env.INTANGLE_CLAUDE_CHANNEL_ORGANIZATION_ID),
22
+ };
23
+ }
24
+ export function claudeChannelToolDefinition() {
25
+ return {
26
+ name: CLAUDE_CHANNEL_REPLY_TOOL,
27
+ title: "Reply to Tan in this Intangle Room",
28
+ description: "Send a reply to Tan in the explicitly configured Intangle conversation. The Space and conversation are fixed by the channel configuration.",
29
+ inputSchema: {
30
+ type: "object",
31
+ additionalProperties: false,
32
+ properties: {
33
+ content: { type: "string", minLength: 1 },
34
+ },
35
+ required: ["content"],
36
+ },
37
+ annotations: { openWorldHint: true },
38
+ };
39
+ }
40
+ export async function sendClaudeChannelReply(config, content, sendReply, createTurnId) {
41
+ const normalized = nonblank(content);
42
+ if (!normalized)
43
+ throw new Error("content is required");
44
+ return sendReply({
45
+ space_id: config.spaceId,
46
+ conversation_id: config.conversationId,
47
+ content: normalized,
48
+ participant_id: config.participantId,
49
+ participant_type: config.participantType,
50
+ participant_label: config.participantLabel,
51
+ ...(config.organizationId
52
+ ? { organization_id: config.organizationId }
53
+ : {}),
54
+ client_turn_id: createTurnId(),
55
+ });
56
+ }
57
+ export class ClaudeChannelBridge {
58
+ options;
59
+ cursor = null;
60
+ initialCursor = null;
61
+ baselineLoaded = false;
62
+ socket = null;
63
+ reconnectTimer = null;
64
+ stopped = false;
65
+ connecting = false;
66
+ messageQueue = Promise.resolve();
67
+ delivered = new Map();
68
+ createSocket;
69
+ schedule;
70
+ report;
71
+ constructor(options) {
72
+ this.options = options;
73
+ this.createSocket =
74
+ options.createSocket ||
75
+ ((url) => new WebSocket(url));
76
+ this.schedule =
77
+ options.schedule || ((callback, delayMs) => setTimeout(callback, delayMs));
78
+ this.report =
79
+ options.report ||
80
+ ((status, detail) => {
81
+ const suffix = detail ? `: ${detail}` : "";
82
+ console.error(`[Intangle Claude channel] ${status}${suffix}`);
83
+ });
84
+ }
85
+ async start() {
86
+ if (!this.options.createSocket &&
87
+ typeof globalThis.WebSocket !== "function") {
88
+ throw new Error(CLAUDE_CHANNEL_RUNTIME_ERROR);
89
+ }
90
+ await this.connect();
91
+ }
92
+ stop() {
93
+ this.stopped = true;
94
+ if (this.reconnectTimer)
95
+ clearTimeout(this.reconnectTimer);
96
+ this.reconnectTimer = null;
97
+ this.socket?.close();
98
+ this.socket = null;
99
+ }
100
+ async connect() {
101
+ if (this.stopped || this.connecting)
102
+ return;
103
+ this.connecting = true;
104
+ try {
105
+ if (!this.baselineLoaded)
106
+ await this.loadBaseline();
107
+ if (this.stopped)
108
+ return;
109
+ const bootstrap = await this.fetchBootstrap();
110
+ if (this.stopped)
111
+ return;
112
+ const socketUrl = new URL(bootstrap.watch_url);
113
+ if (this.cursor !== null) {
114
+ socketUrl.searchParams.set("since", String(Math.max(0, this.cursor - REPLAY_OVERLAP_MS)));
115
+ }
116
+ const socket = this.createSocket(socketUrl.toString());
117
+ this.socket = socket;
118
+ socket.addEventListener("open", () => {
119
+ if (this.socket === socket) {
120
+ this.report("watch_connected", this.options.config.conversationId);
121
+ }
122
+ });
123
+ socket.addEventListener("message", (event) => {
124
+ this.messageQueue = this.messageQueue
125
+ .then(() => this.handleFrame(event.data))
126
+ .catch((error) => {
127
+ this.report("channel_notification_failed", errorText(error));
128
+ socket.close();
129
+ });
130
+ });
131
+ socket.addEventListener("error", (event) => {
132
+ this.report("watch_connection_error", errorText(event));
133
+ });
134
+ socket.addEventListener("close", () => {
135
+ if (this.socket === socket)
136
+ this.socket = null;
137
+ if (!this.stopped) {
138
+ this.report("watch_disconnected", "reconnecting");
139
+ this.scheduleReconnect(bootstrap.watch_keepalive_seconds);
140
+ }
141
+ });
142
+ }
143
+ catch (error) {
144
+ this.report("watch_start_failed", errorText(error));
145
+ this.scheduleReconnect(CHANNEL_RECONNECT_SECONDS);
146
+ }
147
+ finally {
148
+ this.connecting = false;
149
+ }
150
+ }
151
+ async loadBaseline() {
152
+ const result = asRecord(await this.options.api.fetchMessages({
153
+ space_id: this.options.config.spaceId,
154
+ conversation_id: this.options.config.conversationId,
155
+ ...(this.options.config.organizationId
156
+ ? { organization_id: this.options.config.organizationId }
157
+ : {}),
158
+ limit: 1,
159
+ }));
160
+ if (result?.success !== true ||
161
+ result.conversation_id !== this.options.config.conversationId ||
162
+ !Array.isArray(result.messages)) {
163
+ throw new Error("Conversation baseline fetch failed");
164
+ }
165
+ this.cursor = result.messages.reduce((latest, value) => {
166
+ const message = asRecord(value);
167
+ const durableTimestamp = message?.durable_timestamp;
168
+ if (typeof durableTimestamp !== "number" ||
169
+ !Number.isFinite(durableTimestamp)) {
170
+ return latest;
171
+ }
172
+ return latest === null
173
+ ? durableTimestamp
174
+ : Math.max(latest, durableTimestamp);
175
+ }, null);
176
+ this.initialCursor = this.cursor;
177
+ this.baselineLoaded = true;
178
+ }
179
+ async fetchBootstrap() {
180
+ const result = asRecord(await this.options.api.subscribe({
181
+ space_id: this.options.config.spaceId,
182
+ conversation_id: this.options.config.conversationId,
183
+ participant_id: this.options.config.participantId,
184
+ participant_type: this.options.config.participantType,
185
+ participant_label: this.options.config.participantLabel,
186
+ ...(this.options.config.organizationId
187
+ ? { organization_id: this.options.config.organizationId }
188
+ : {}),
189
+ mark_read: false,
190
+ include_watch: true,
191
+ }));
192
+ if (result?.success !== true ||
193
+ result.conversation_id !== this.options.config.conversationId ||
194
+ typeof result.watch_url !== "string" ||
195
+ typeof result.watch_expires_in_seconds !== "number" ||
196
+ typeof result.watch_keepalive_seconds !== "number" ||
197
+ typeof result.watch_max_lifetime_seconds !== "number" ||
198
+ !isWatchCursor(result.watch_cursor)) {
199
+ throw new Error("Conversation subscription omitted its watch bootstrap");
200
+ }
201
+ return result;
202
+ }
203
+ async handleFrame(raw) {
204
+ const text = typeof raw === "string" ? raw : String(raw);
205
+ let frame;
206
+ try {
207
+ frame = JSON.parse(text);
208
+ }
209
+ catch {
210
+ this.report("watch_frame_invalid", "ignored non-JSON frame");
211
+ return;
212
+ }
213
+ if (frame.type !== "message" ||
214
+ frame.space_id !== this.options.config.spaceId ||
215
+ frame.conversation_id !== this.options.config.conversationId ||
216
+ typeof frame.message_id !== "string" ||
217
+ typeof frame.durable_timestamp !== "number" ||
218
+ frame.transient === true ||
219
+ frame.role !== "assistant" ||
220
+ frame.source !== "tan_agent_message" ||
221
+ frame.recipient_id !== this.options.config.participantId ||
222
+ typeof frame.content !== "string") {
223
+ return;
224
+ }
225
+ if (this.initialCursor !== null &&
226
+ frame.durable_timestamp <= this.initialCursor) {
227
+ return;
228
+ }
229
+ const messageId = frame.message_id;
230
+ const content = frame.content;
231
+ const notificationContent = await this.resolveNotificationContent({
232
+ message_id: messageId,
233
+ content,
234
+ content_truncated: frame.content_truncated,
235
+ });
236
+ const fingerprint = notificationContent.content;
237
+ const previous = this.delivered.get(frame.message_id);
238
+ if (previous?.cursor === frame.durable_timestamp &&
239
+ previous.fingerprint === fingerprint)
240
+ return;
241
+ await this.options.sink.notification({
242
+ method: "notifications/claude/channel",
243
+ params: {
244
+ content: notificationContent.content,
245
+ meta: {
246
+ conversation_id: this.options.config.conversationId,
247
+ message_id: frame.message_id,
248
+ sender: "Tan",
249
+ recipient_id: this.options.config.participantId,
250
+ ...(notificationContent.truncated
251
+ ? {
252
+ content_truncated: "true",
253
+ delivery: "full_message_unavailable",
254
+ }
255
+ : {}),
256
+ },
257
+ },
258
+ });
259
+ this.delivered.set(frame.message_id, {
260
+ cursor: frame.durable_timestamp,
261
+ fingerprint,
262
+ });
263
+ this.cursor =
264
+ this.cursor === null
265
+ ? frame.durable_timestamp
266
+ : Math.max(this.cursor, frame.durable_timestamp);
267
+ const cutoff = Math.max(0, this.cursor - REPLAY_OVERLAP_MS);
268
+ for (const [messageId, projection] of this.delivered) {
269
+ if (projection.cursor < cutoff)
270
+ this.delivered.delete(messageId);
271
+ }
272
+ // MCP notifications have no consumer acknowledgement. This is transport
273
+ // write evidence only, never a claim that Claude processed the event.
274
+ this.report("notification_write_completed_unacknowledged", frame.message_id);
275
+ }
276
+ async resolveNotificationContent(frame) {
277
+ if (frame.content_truncated !== true) {
278
+ return { content: frame.content, truncated: false };
279
+ }
280
+ let result;
281
+ try {
282
+ result = asRecord(await this.options.api.fetchMessages({
283
+ space_id: this.options.config.spaceId,
284
+ conversation_id: this.options.config.conversationId,
285
+ ...(this.options.config.organizationId
286
+ ? { organization_id: this.options.config.organizationId }
287
+ : {}),
288
+ limit: 200,
289
+ }));
290
+ }
291
+ catch (error) {
292
+ this.report("message_content_fetch_failed", errorText(error));
293
+ return this.truncatedMessageWarning(frame.message_id);
294
+ }
295
+ const fullMessage = result?.success === true &&
296
+ result.conversation_id === this.options.config.conversationId &&
297
+ Array.isArray(result.messages)
298
+ ? result.messages.find((message) => asRecord(message)?.id === frame.message_id)
299
+ : undefined;
300
+ const fullRecord = asRecord(fullMessage);
301
+ const fullContent = fullRecord ? messageText(fullRecord) : undefined;
302
+ if (typeof fullContent === "string" &&
303
+ fullContent.length > frame.content.length) {
304
+ return { content: fullContent, truncated: false };
305
+ }
306
+ return this.truncatedMessageWarning(frame.message_id);
307
+ }
308
+ truncatedMessageWarning(messageId) {
309
+ this.report("message_content_unavailable", messageId);
310
+ return {
311
+ content: `Tan's message ${messageId} exceeded the live channel limit, and its full text could not be retrieved. Do not act on partial text. Ask Tan to resend the message in a shorter form.`,
312
+ truncated: true,
313
+ };
314
+ }
315
+ scheduleReconnect(seconds) {
316
+ if (this.stopped || this.reconnectTimer)
317
+ return;
318
+ const delaySeconds = Number.isFinite(seconds) && seconds > 0
319
+ ? seconds
320
+ : CHANNEL_RECONNECT_SECONDS;
321
+ this.reconnectTimer = this.schedule(() => {
322
+ this.reconnectTimer = null;
323
+ void this.connect();
324
+ }, delaySeconds * 1_000);
325
+ }
326
+ }
327
+ function isWatchCursor(value) {
328
+ const cursor = asRecord(value);
329
+ return (cursor?.parameter === "since" &&
330
+ cursor.semantics === "exclusive durable replay cursor");
331
+ }
332
+ function asRecord(value) {
333
+ return typeof value === "object" && value !== null && !Array.isArray(value)
334
+ ? value
335
+ : null;
336
+ }
337
+ function nonblank(value) {
338
+ return typeof value === "string" && value.trim() !== ""
339
+ ? value.trim()
340
+ : undefined;
341
+ }
342
+ function messageText(message) {
343
+ if (typeof message.content === "string" && message.content !== "") {
344
+ return message.content;
345
+ }
346
+ if (!Array.isArray(message.parts))
347
+ return undefined;
348
+ const text = message.parts
349
+ .filter((part) => {
350
+ const record = asRecord(part);
351
+ return record?.type === "text" && typeof record.text === "string";
352
+ })
353
+ .map((part) => String(asRecord(part)?.text))
354
+ .join("");
355
+ return text || undefined;
356
+ }
357
+ function errorText(error) {
358
+ return error instanceof Error ? error.message : String(error);
359
+ }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * Intangle MCP Server - Exposes Intangle context functionality through MCP
4
4
  */
5
+ import { randomUUID } from "node:crypto";
5
6
  import { appendFileSync, readFileSync } from "node:fs";
6
7
  import { dirname, join } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
@@ -10,6 +11,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
10
11
  import { CallToolRequestSchema, ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
12
  import { config } from "dotenv";
12
13
  import fetch from "node-fetch";
14
+ import { CLAUDE_CHANNEL_ENABLED_ENV, CLAUDE_CHANNEL_REPLY_TOOL, CLAUDE_CHANNEL_RUNTIME_ERROR, ClaudeChannelBridge, claudeChannelConfigFromEnv, claudeChannelToolDefinition, sendClaudeChannelReply, } from "./claude-channel.js";
13
15
  import { HIGHQ_DIRECT_TOOL_NAMES } from "./highq-tool-definitions.js";
14
16
  const HIGHQ_DIRECT_TOOL_NAME_SET = new Set(HIGHQ_DIRECT_TOOL_NAMES);
15
17
  // ... imports ...
@@ -33,6 +35,7 @@ try {
33
35
  const API_BASE_URL = process.env.NEXT_APP_URL || "https://renvoi.co/intangle";
34
36
  const MCP_API_KEY = process.env.MCP_API_KEY;
35
37
  const VERCEL_BYPASS_TOKEN = process.env.VERCEL_BYPASS_TOKEN || process.env.VERCEL_PROTECTION_BYPASS;
38
+ const claudeChannelConfig = claudeChannelConfigFromEnv(process.env);
36
39
  log(`CWD: ${process.cwd()}`);
37
40
  log(`Has API Key: ${!!MCP_API_KEY}`);
38
41
  log(`Has Vercel Bypass Token: ${!!VERCEL_BYPASS_TOKEN}`);
@@ -250,14 +253,26 @@ try {
250
253
  capabilities: {
251
254
  resources: {},
252
255
  tools: {},
256
+ ...(claudeChannelConfig
257
+ ? { experimental: { "claude/channel": {} } }
258
+ : {}),
253
259
  },
260
+ ...(claudeChannelConfig
261
+ ? {
262
+ instructions: "Messages arrive from Tan in the explicitly configured Intangle conversation as a <channel> notification. Treat them as live requests in this session. Reply to Tan in this same Room only by calling intangle_channel_reply with the reply content. This channel delivers new messages only; it does not replay prior conversation history.",
263
+ }
264
+ : {}),
254
265
  });
255
266
  server.setRequestHandler(ListToolsRequestSchema, async () => {
256
267
  const result = await makeRemoteRpcCall("tools/list", {});
257
268
  if (!Array.isArray(result?.tools)) {
258
269
  throw new McpError(ErrorCode.InternalError, "Remote tool catalog unavailable");
259
270
  }
260
- return { tools: result.tools };
271
+ return {
272
+ tools: claudeChannelConfig
273
+ ? [...result.tools, claudeChannelToolDefinition()]
274
+ : result.tools,
275
+ };
261
276
  });
262
277
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
263
278
  const result = await makeRemoteRpcCall("resources/list", {});
@@ -470,6 +485,12 @@ try {
470
485
  try {
471
486
  let result;
472
487
  switch (name) {
488
+ case CLAUDE_CHANNEL_REPLY_TOOL:
489
+ if (!claudeChannelConfig) {
490
+ throw new McpError(ErrorCode.InvalidRequest, `${CLAUDE_CHANNEL_ENABLED_ENV} is not enabled for this server process`);
491
+ }
492
+ result = await sendClaudeChannelReply(claudeChannelConfig, args?.content, handleMessage, randomUUID);
493
+ break;
473
494
  case "search":
474
495
  result = await handleSearchContext(args);
475
496
  break;
@@ -599,9 +620,27 @@ try {
599
620
  }
600
621
  });
601
622
  async function main() {
623
+ if (claudeChannelConfig && typeof globalThis.WebSocket !== "function") {
624
+ console.error(CLAUDE_CHANNEL_RUNTIME_ERROR);
625
+ process.exit(1);
626
+ }
602
627
  const transport = new StdioServerTransport();
603
628
  await server.connect(transport);
604
629
  log("Server connected to transport");
630
+ if (claudeChannelConfig) {
631
+ const channel = new ClaudeChannelBridge({
632
+ config: claudeChannelConfig,
633
+ api: {
634
+ fetchMessages: handleFetchConversationMessages,
635
+ subscribe: handleSubscribeConversation,
636
+ sendReply: handleMessage,
637
+ },
638
+ sink: server,
639
+ });
640
+ await channel.start();
641
+ process.once("SIGINT", () => channel.stop());
642
+ process.once("SIGTERM", () => channel.stop());
643
+ }
605
644
  // Client info is captured on-demand in ensureClientInfo() before first API call
606
645
  }
607
646
  main().catch((err) => {
@@ -159,11 +159,20 @@ const BASE_TOOLS = [
159
159
  {
160
160
  name: "get_items",
161
161
  title: "Get Items",
162
- description: "Read complete Items by exact IDs or browse below one Space/Item using authoritative parent_id hierarchy. Every Item is a Room; Spaces remain typed scopes. Legacy IDs are accepted with alias provenance preserved. Use include=comments for canonical Item-owned comments and body revision. Use include=assignees for typed User/Agent principals.",
162
+ description: "For whole-Space organisation, use mode=structure. It returns complete authorised structural aggregates plus compact paged Item details; aggregates cover the full scope. Omit ids and depth, and omit under to inspect the whole authorised Space. Use coverage.snapshot, a structural SHA-256 checksum over authoritative inventory, to compare pages; it does not make page hydration transactional. Read complete Items by exact IDs or browse below one Space/Item using authoritative parent_id hierarchy. Every Item is a Room; Spaces remain typed scopes. Legacy IDs are accepted with alias provenance preserved. Use include=comments for canonical Item-owned comments and body revision. Use include=assignees for typed User/Agent principals.",
163
163
  inputSchema: {
164
164
  type: "object",
165
165
  additionalProperties: false,
166
166
  properties: {
167
+ mode: {
168
+ type: "string",
169
+ enum: [
170
+ "items",
171
+ "structure"
172
+ ],
173
+ default: "items",
174
+ description: "items (default) returns bounded Item reads. structure returns complete authorised structural aggregates plus compact paged Item details; omit ids and depth. Omit under to inspect the whole authorised Space."
175
+ },
167
176
  space_id: {
168
177
  type: "string",
169
178
  description: "REQUIRED: Exact Space containing the Items."
@@ -188,17 +197,20 @@ const BASE_TOOLS = [
188
197
  depth: {
189
198
  type: "integer",
190
199
  minimum: 0,
191
- maximum: 20
200
+ maximum: 20,
201
+ description: "Maximum hierarchy depth. Defaults to 0 for current Space roots and 1 for explicit selectors. Omit in structure mode, which inspects all depths."
192
202
  },
193
203
  limit: {
194
204
  type: "integer",
195
205
  minimum: 1,
196
- maximum: 200
206
+ maximum: 200,
207
+ default: 50,
208
+ description: "Maximum Items in this page."
197
209
  },
198
210
  offset: {
199
211
  type: "integer",
200
212
  minimum: 0,
201
- maximum: 5000
213
+ description: "Items to skip in stable breadth-first order. items mode supports up to 5000; structure has no offset cap."
202
214
  },
203
215
  include: {
204
216
  type: "array",
@@ -216,7 +228,8 @@ const BASE_TOOLS = [
216
228
  "metadata",
217
229
  "all"
218
230
  ]
219
- }
231
+ },
232
+ description: "Optional payload fields for the returned page. structure defaults to []; items defaults to content, attributes, assignee, artifact, and metadata. Explicit include hydrates only selected page details, never aggregate content coverage."
220
233
  }
221
234
  },
222
235
  required: [
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@intangle/mcp-server",
3
- "version": "2.6.18",
3
+ "version": "2.6.20",
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",
7
7
  "scripts": {
8
8
  "start": "node dist/index.js",
9
9
  "dev": "tsx watch index.ts",
10
+ "test": "tsx --test tests/*.test.ts",
10
11
  "prebuild": "node scripts/sync-tool-definitions.mjs",
11
12
  "build": "tsc",
12
13
  "lint": "biome check .",
@@ -55,5 +56,5 @@
55
56
  "tsx": "^4.0.0",
56
57
  "typescript": "^5.0.0"
57
58
  },
58
- "intangleSourceHash": "b0f174c2303048a64be3fb5fcfa162856d98b5f34a05ddbcf7813263f6820fbe"
59
+ "intangleSourceHash": "8cbc22fac71d3139119b38680db7337458c57e1a36d23033969618c3b8458dcb"
59
60
  }