@pipeshub-ai/mcp 2.2.0 → 2.3.0
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 +2 -0
- package/bin/mcp-server.js +648 -175
- package/bin/mcp-server.js.map +17 -11
- package/bin/pipeshub.js +946 -0
- package/bin/pipeshub.js.map +14 -0
- package/esm/cli/client.d.ts +54 -0
- package/esm/cli/client.d.ts.map +1 -0
- package/esm/cli/client.js +230 -0
- package/esm/cli/client.js.map +1 -0
- package/esm/cli/commands.d.ts +30 -0
- package/esm/cli/commands.d.ts.map +1 -0
- package/esm/cli/commands.js +321 -0
- package/esm/cli/commands.js.map +1 -0
- package/esm/cli/config.d.ts +73 -0
- package/esm/cli/config.d.ts.map +1 -0
- package/esm/cli/config.js +218 -0
- package/esm/cli/config.js.map +1 -0
- package/esm/cli/init-qm.d.ts +11 -0
- package/esm/cli/init-qm.d.ts.map +1 -0
- package/esm/cli/init-qm.js +170 -0
- package/esm/cli/init-qm.js.map +1 -0
- package/esm/cli/pipeshub.d.ts +2 -0
- package/esm/cli/pipeshub.d.ts.map +1 -0
- package/esm/cli/pipeshub.js +296 -0
- package/esm/cli/pipeshub.js.map +1 -0
- package/esm/funcs/connectorLookupRecordByIdentifier.d.ts +68 -0
- package/esm/funcs/connectorLookupRecordByIdentifier.d.ts.map +1 -0
- package/esm/funcs/connectorLookupRecordByIdentifier.js +138 -0
- package/esm/funcs/connectorLookupRecordByIdentifier.js.map +1 -0
- package/esm/funcs/connectorNavigateKnowledgeGraph.d.ts +75 -0
- package/esm/funcs/connectorNavigateKnowledgeGraph.d.ts.map +1 -0
- package/esm/funcs/connectorNavigateKnowledgeGraph.js +152 -0
- package/esm/funcs/connectorNavigateKnowledgeGraph.js.map +1 -0
- package/esm/lib/base64.d.ts +1 -1
- package/esm/lib/base64.d.ts.map +1 -1
- package/esm/lib/base64.js +6 -3
- package/esm/lib/base64.js.map +1 -1
- package/esm/lib/bytes-to-base64.d.ts +16 -0
- package/esm/lib/bytes-to-base64.d.ts.map +1 -0
- package/esm/lib/bytes-to-base64.js +23 -0
- package/esm/lib/bytes-to-base64.js.map +1 -0
- package/esm/mcp-server/instructions.d.ts +1 -1
- package/esm/mcp-server/instructions.d.ts.map +1 -1
- package/esm/mcp-server/instructions.js +30 -0
- package/esm/mcp-server/instructions.js.map +1 -1
- package/esm/mcp-server/shared.d.ts +2 -2
- package/esm/mcp-server/shared.d.ts.map +1 -1
- package/esm/mcp-server/shared.js +7 -3
- package/esm/mcp-server/shared.js.map +1 -1
- package/esm/mcp-server/tools/_agui.d.ts +34 -0
- package/esm/mcp-server/tools/_agui.d.ts.map +1 -0
- package/esm/mcp-server/tools/_agui.js +91 -0
- package/esm/mcp-server/tools/_agui.js.map +1 -0
- package/esm/mcp-server/tools/_helpers.d.ts.map +1 -1
- package/esm/mcp-server/tools/_helpers.js +13 -6
- package/esm/mcp-server/tools/_helpers.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubChat.d.ts +0 -3
- package/esm/mcp-server/tools/pipeshubChat.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubChat.js +75 -98
- package/esm/mcp-server/tools/pipeshubChat.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts +118 -1
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubGetRecordContent.js +180 -29
- package/esm/mcp-server/tools/pipeshubGetRecordContent.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubSearch.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubSearch.js +14 -1
- package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
- package/esm/models/availablemodelsresponse.d.ts +1 -1
- package/esm/models/conversation.d.ts +1 -1
- package/esm/models/lookuprecordbyidentifierop.d.ts +7 -0
- package/esm/models/lookuprecordbyidentifierop.d.ts.map +1 -0
- package/esm/models/lookuprecordbyidentifierop.js +14 -0
- package/esm/models/lookuprecordbyidentifierop.js.map +1 -0
- package/esm/models/navigateknowledgegraphop.d.ts +14 -0
- package/esm/models/navigateknowledgegraphop.d.ts.map +1 -0
- package/esm/models/navigateknowledgegraphop.js +25 -0
- package/esm/models/navigateknowledgegraphop.js.map +1 -0
- package/esm/models/userteamsresponse.d.ts +1 -1
- package/esm/tool-names.js +3 -3
- package/esm/tool-names.js.map +1 -1
- package/package.json +3 -2
- package/qm/README.md +247 -0
- package/qm/SECURITY.md +134 -0
- package/qm/TROUBLESHOOTING.md +218 -0
- package/qm/qm.config.fragment.jsonc +42 -0
- package/qm/sandbox/Dockerfile +22 -0
- package/qm/sandbox/skills/pipeshub/SKILL.md +69 -0
- package/qm/sandbox/tools/pipeshub/tool.json +36 -0
- package/src/cli/client.ts +272 -0
- package/src/cli/commands.ts +403 -0
- package/src/cli/config.ts +229 -0
- package/src/cli/init-qm.ts +215 -0
- package/src/cli/pipeshub.ts +333 -0
- package/src/funcs/connectorLookupRecordByIdentifier.ts +200 -0
- package/src/funcs/connectorNavigateKnowledgeGraph.ts +215 -0
- package/src/lib/base64.ts +6 -3
- package/src/lib/bytes-to-base64.ts +24 -0
- package/src/mcp-server/instructions.ts +30 -0
- package/src/mcp-server/shared.ts +7 -3
- package/src/mcp-server/tools/_agui.ts +138 -0
- package/src/mcp-server/tools/_helpers.ts +13 -6
- package/src/mcp-server/tools/pipeshubChat.ts +78 -97
- package/src/mcp-server/tools/pipeshubGetRecordContent.ts +213 -29
- package/src/mcp-server/tools/pipeshubSearch.ts +14 -1
- package/src/models/lookuprecordbyidentifierop.ts +26 -0
- package/src/models/navigateknowledgegraphop.ts +56 -0
- package/src/tool-names.ts +3 -3
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
// `POST /conversations/stream` (and
|
|
2
2
|
// `POST /conversations/{conversationId}/messages/stream` for follow-ups),
|
|
3
3
|
// accumulate the frames server-side, and hand the LLM a single trimmed
|
|
4
|
-
// `CallToolResult` once the stream
|
|
5
|
-
// `error`) frame.
|
|
4
|
+
// `CallToolResult` once the stream reaches a terminal frame.
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// `accumulated` as a fallback in case the stream
|
|
13
|
-
// ends without a `complete` frame
|
|
14
|
-
// - tool_call — model invoked an internal tool; we collect these
|
|
15
|
-
// as observability breadcrumbs but the LLM doesn't
|
|
16
|
-
// need them to answer
|
|
17
|
-
// - tool_success — paired with tool_call; same treatment
|
|
18
|
-
// - complete — terminal; payload is `{ conversation, meta }`,
|
|
19
|
-
// matching the non-stream response shape, so we
|
|
20
|
-
// feed it through `trimConversation` like before
|
|
21
|
-
// - error — terminal; surface to the LLM as a tool error
|
|
6
|
+
// The wire protocol is AG-UI. The legacy `connected` / `answer_chunk` /
|
|
7
|
+
// `complete` / `error` vocabulary was removed with the new agent loop and is
|
|
8
|
+
// never emitted. The frames that matter — CUSTOM{conversation_created},
|
|
9
|
+
// TEXT_MESSAGE_CONTENT, RUN_FINISHED, RUN_ERROR — are folded by `./_agui.js`,
|
|
10
|
+
// which is kept separate so the fold stays a pure function over frames.
|
|
22
11
|
|
|
23
12
|
import * as z from "zod";
|
|
24
13
|
import { conversationsStreamConversation } from "../../funcs/conversationsStreamConversation.js";
|
|
@@ -26,6 +15,11 @@ import { conversationsStreamMessage } from "../../funcs/conversationsStreamMessa
|
|
|
26
15
|
import { agentsStreamConversation } from "../../funcs/agentsStreamConversation.js";
|
|
27
16
|
import { agentsStreamMessage } from "../../funcs/agentsStreamMessage.js";
|
|
28
17
|
import { ToolDefinition } from "../tools.js";
|
|
18
|
+
import {
|
|
19
|
+
applyAGUIFrame,
|
|
20
|
+
newChatStreamState,
|
|
21
|
+
salvagedText,
|
|
22
|
+
} from "./_agui.js";
|
|
29
23
|
import {
|
|
30
24
|
errorResult,
|
|
31
25
|
httpErrorResult,
|
|
@@ -36,9 +30,11 @@ import {
|
|
|
36
30
|
|
|
37
31
|
const FiltersShape = z.object({
|
|
38
32
|
apps: z.array(z.string()).optional().describe(
|
|
39
|
-
"Source-scoping ids
|
|
40
|
-
+ "
|
|
41
|
-
+ "
|
|
33
|
+
"Source-scoping ids from `pipeshub_sources` — connector instance and / "
|
|
34
|
+
+ "or knowledge base ids, mixed freely. The legacy org-wide "
|
|
35
|
+
+ "`knowledgeBase_<orgId>` id is still accepted on deployments that "
|
|
36
|
+
+ "predate per-KB sources. Empty / omitted means no app-side "
|
|
37
|
+
+ "restriction.",
|
|
42
38
|
),
|
|
43
39
|
kb: z.array(z.string()).optional().describe(
|
|
44
40
|
"Legacy / unused. Leave empty.",
|
|
@@ -76,48 +72,53 @@ const args = {
|
|
|
76
72
|
chatMode: z.enum([
|
|
77
73
|
"internal_search",
|
|
78
74
|
"web_search",
|
|
79
|
-
"auto",
|
|
80
75
|
"quick",
|
|
81
|
-
"verification",
|
|
82
|
-
"deep",
|
|
83
76
|
]).optional().describe(
|
|
84
77
|
"Response strategy. The valid values depend on whether `agentId` is set:\n"
|
|
85
78
|
+ "- WITHOUT `agentId` (plain chat): `internal_search` — answer from the "
|
|
86
79
|
+ "org's indexed knowledge (default) — or `web_search` — answer from the "
|
|
87
80
|
+ "live web.\n"
|
|
88
|
-
+ "- WITH `agentId` (agent chat): `
|
|
89
|
-
+ "
|
|
81
|
+
+ "- WITH `agentId` (agent chat): `quick` is the only supported mode and "
|
|
82
|
+
+ "is sent automatically, so this argument can be omitted.",
|
|
90
83
|
),
|
|
91
84
|
};
|
|
92
85
|
|
|
93
86
|
export const tool$pipeshubChat: ToolDefinition<typeof args> = {
|
|
94
87
|
name: "pipeshub_chat",
|
|
95
88
|
description:
|
|
96
|
-
|
|
89
|
+
`Ask a question, get an answer grounded in the org's indexed data with
|
|
90
|
+
citations. It reads a few retrieved passages — never a whole document,
|
|
91
|
+
never a complete list.
|
|
92
|
+
|
|
93
|
+
**Three questions this tool gets WRONG. Check them first:**
|
|
94
|
+
- **Structure** — "what's under this epic?", "which pages are in this
|
|
95
|
+
space?", "what links to this ticket?", "what's in this folder?" →
|
|
96
|
+
\`pipeshub_get_record_content\` \`mode:"navigate"\`. Ranking cannot see how
|
|
97
|
+
records relate.
|
|
98
|
+
- **Exhaustive** — "how many X?", "list ALL the Y", "every Z" →
|
|
99
|
+
\`mode:"navigate"\`, which reports the group's real total. This tool
|
|
100
|
+
undercounts and will not say so.
|
|
101
|
+
- **One named document** — summarize it, extract from it, what does it say
|
|
102
|
+
about X → \`pipeshub_search\` for the \`recordId\`, then \`mode:"content"\`.
|
|
103
|
+
|
|
104
|
+
Everything else about the org's knowledge belongs here: policies,
|
|
105
|
+
processes, decisions, history, "what do we know about X", and any question
|
|
106
|
+
spanning several documents.
|
|
97
107
|
|
|
98
|
-
**Internal search** (default, \`chatMode: "internal_search"\`):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Grounds the answer in the user's actual data and returns citations.
|
|
103
|
-
Answers come from a few retrieved passages, not whole documents — for
|
|
104
|
-
any task needing a document's full content, use
|
|
105
|
-
\`pipeshub_get_record_content\` instead.
|
|
108
|
+
**Internal search** (default, \`chatMode: "internal_search"\`): the user's
|
|
109
|
+
documents, files, knowledge base, company policies — anything in their
|
|
110
|
+
PipesHub-indexed sources (Drive, Box, Confluence, Slack, Gmail, Jira, the
|
|
111
|
+
org's KB, ...).
|
|
106
112
|
|
|
107
|
-
**Web search** (\`chatMode: "web_search"\`):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
**Web search** (\`chatMode: "web_search"\`): current events or public
|
|
114
|
+
information unlikely to be in the org's knowledge base.
|
|
115
|
+
|
|
116
|
+
Both are plain-chat modes. **Agent chat** — pass an \`agentId\` from
|
|
117
|
+
\`pipeshub_agents\` — runs against that agent's own prompt, tools and knowledge;
|
|
118
|
+
\`quick\` is its only mode, requires the \`agentId\`, and is sent automatically.
|
|
111
119
|
|
|
112
|
-
**When to pick this over other tools:**
|
|
113
|
-
- "Summarize <doc>" / "key points of <doc>" / "what does <document> say
|
|
114
|
-
about X?" → NOT this tool. Use \`pipeshub_search\` →
|
|
115
|
-
\`pipeshub_get_record_content\`: answering for a specific document
|
|
116
|
-
requires its full content, and chat only sees a few retrieved
|
|
117
|
-
passages, never the whole document.
|
|
118
120
|
- "What's our policy on Y?" → \`pipeshub_chat\` (internal_search)
|
|
119
121
|
- "What's in the news about Z?" → \`pipeshub_chat\` (web_search)
|
|
120
|
-
- "What is the latest version of <library>?" → \`pipeshub_chat\` (web_search)
|
|
121
122
|
- "Find / locate the file named X" → \`pipeshub_search\` (then
|
|
122
123
|
\`pipeshub_download_record\` if the user wants the bytes).
|
|
123
124
|
|
|
@@ -147,11 +148,19 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
147
148
|
args,
|
|
148
149
|
tool: async (client, args, ctx) => {
|
|
149
150
|
const fetchOptions = { signal: ctx.signal };
|
|
151
|
+
// `quick` is agent-only. The plain stream schemas accept internal_search /
|
|
152
|
+
// web_search / agent, so collapse anything else to the default rather than
|
|
153
|
+
// forwarding a value that path would reject.
|
|
154
|
+
const plainChatMode = args.chatMode === "web_search"
|
|
155
|
+
? "web_search"
|
|
156
|
+
: "internal_search";
|
|
150
157
|
let response: Response;
|
|
151
158
|
|
|
152
159
|
if (args.agentId) {
|
|
153
|
-
//
|
|
154
|
-
|
|
160
|
+
// `quick` is the only value the agent stream schemas accept, and it is
|
|
161
|
+
// required — so ignore whatever the caller passed rather than forwarding
|
|
162
|
+
// a value the gateway would reject.
|
|
163
|
+
const agentChatMode = "quick" as const;
|
|
155
164
|
if (args.conversationId) {
|
|
156
165
|
// Continue an existing agent conversation.
|
|
157
166
|
const [result] = await agentsStreamMessage(client, {
|
|
@@ -192,7 +201,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
192
201
|
modelKey: args.modelKey,
|
|
193
202
|
modelName: args.modelName,
|
|
194
203
|
modelFriendlyName: args.modelFriendlyName,
|
|
195
|
-
chatMode:
|
|
204
|
+
chatMode: plainChatMode,
|
|
196
205
|
},
|
|
197
206
|
}, { fetchOptions }).$inspect();
|
|
198
207
|
if (!result.ok) return errorResult(result.error.message);
|
|
@@ -205,7 +214,7 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
205
214
|
modelKey: args.modelKey,
|
|
206
215
|
modelName: args.modelName,
|
|
207
216
|
modelFriendlyName: args.modelFriendlyName,
|
|
208
|
-
chatMode:
|
|
217
|
+
chatMode: plainChatMode,
|
|
209
218
|
}, { fetchOptions }).$inspect();
|
|
210
219
|
if (!result.ok) return errorResult(result.error.message);
|
|
211
220
|
response = result.value;
|
|
@@ -217,72 +226,44 @@ cited document, take \`citations[*].recordId\` and call
|
|
|
217
226
|
const httpErr = await httpErrorResult(response, "PipesHub chat request");
|
|
218
227
|
if (httpErr) return httpErr;
|
|
219
228
|
|
|
220
|
-
// Drain the
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
let lastAccumulated: string | null = null;
|
|
225
|
-
let errorMessage: string | null = null;
|
|
226
|
-
|
|
229
|
+
// Drain the AG-UI stream. `applyAGUIFrame` returns true only on a
|
|
230
|
+
// genuinely terminal frame — a sub-agent's RUN_FINISHED carries no
|
|
231
|
+
// `result` and must not end the loop.
|
|
232
|
+
const state = newChatStreamState();
|
|
227
233
|
try {
|
|
228
234
|
for await (const frame of iterateSSE(response)) {
|
|
229
|
-
|
|
230
|
-
case "complete": {
|
|
231
|
-
const d = frame.data ?? {};
|
|
232
|
-
finalConversation = d.conversation ?? null;
|
|
233
|
-
recordsUsed = d.recordsUsed ?? d.meta?.recordsUsed;
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
case "error": {
|
|
237
|
-
const d = frame.data ?? {};
|
|
238
|
-
errorMessage = typeof d === "string"
|
|
239
|
-
? d
|
|
240
|
-
: (d.error ?? d.message ?? frame.raw ?? "Stream error");
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
case "answer_chunk": {
|
|
244
|
-
const d = frame.data ?? {};
|
|
245
|
-
if (typeof d.accumulated === "string") {
|
|
246
|
-
lastAccumulated = d.accumulated;
|
|
247
|
-
} else if (typeof d.content === "string") {
|
|
248
|
-
lastAccumulated = (lastAccumulated ?? "") + d.content;
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
// status / tool_call / tool_success / connected — ignored.
|
|
253
|
-
default:
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
if (finalConversation || errorMessage) break;
|
|
235
|
+
if (applyAGUIFrame(state, frame)) break;
|
|
257
236
|
}
|
|
258
237
|
} catch (e: unknown) {
|
|
259
238
|
return errorResult(`SSE stream failed: ${(e as Error).message}`);
|
|
260
239
|
}
|
|
261
240
|
|
|
262
|
-
if (
|
|
241
|
+
if (state.error) return errorResult(state.error);
|
|
263
242
|
|
|
264
|
-
if (
|
|
243
|
+
if (state.conversation) {
|
|
265
244
|
return jsonResult({
|
|
266
|
-
...trimConversation(
|
|
267
|
-
recordsUsed,
|
|
245
|
+
...trimConversation(state.conversation),
|
|
246
|
+
recordsUsed: state.recordsUsed,
|
|
268
247
|
});
|
|
269
248
|
}
|
|
270
249
|
|
|
271
|
-
// Stream ended without a terminal frame — unusual, but salvage what
|
|
272
|
-
//
|
|
273
|
-
|
|
250
|
+
// Stream ended without a terminal frame — unusual, but salvage what we
|
|
251
|
+
// accumulated so the LLM has something to work with. `conversation_created`
|
|
252
|
+
// arrives before any answer text, so a follow-up turn can still resume.
|
|
253
|
+
const salvaged = salvagedText(state);
|
|
254
|
+
if (salvaged) {
|
|
274
255
|
return jsonResult({
|
|
275
|
-
conversationId:
|
|
276
|
-
title:
|
|
256
|
+
conversationId: state.conversationId,
|
|
257
|
+
title: state.title,
|
|
277
258
|
status: "Inprogress",
|
|
278
|
-
answer:
|
|
259
|
+
answer: salvaged,
|
|
279
260
|
confidence: null,
|
|
280
261
|
citations: [],
|
|
281
262
|
followUpQuestions: [],
|
|
282
263
|
messageCount: 0,
|
|
283
|
-
recordsUsed,
|
|
284
|
-
warning: "Stream ended without a
|
|
285
|
-
+ "
|
|
264
|
+
recordsUsed: state.recordsUsed,
|
|
265
|
+
warning: "Stream ended without a terminal RUN_FINISHED; answer is the "
|
|
266
|
+
+ "accumulated TEXT_MESSAGE_CONTENT and citations are unavailable.",
|
|
286
267
|
});
|
|
287
268
|
}
|
|
288
269
|
|
|
@@ -1,46 +1,184 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { connectorGetRecordContent } from "../../funcs/connectorGetRecordContent.js";
|
|
3
|
+
import { connectorLookupRecordByIdentifier } from "../../funcs/connectorLookupRecordByIdentifier.js";
|
|
4
|
+
import { connectorNavigateKnowledgeGraph } from "../../funcs/connectorNavigateKnowledgeGraph.js";
|
|
3
5
|
import { ToolDefinition } from "../tools.js";
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
errorResult,
|
|
8
|
+
httpErrorResult,
|
|
9
|
+
jsonResult,
|
|
10
|
+
readJson,
|
|
11
|
+
} from "./_helpers.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Navigate and lookup return a server-rendered `text` view alongside the
|
|
15
|
+
* structured fields. It ends in a `Next:` line naming the exact follow-up call,
|
|
16
|
+
* so hand the LLM that string; fall back to JSON if the field is missing.
|
|
17
|
+
*/
|
|
18
|
+
export async function renderedResult(response: Response) {
|
|
19
|
+
const parsed = await readJson<{ text?: string }>(response);
|
|
20
|
+
if (!parsed.ok) return parsed.result;
|
|
21
|
+
const text = parsed.value?.text;
|
|
22
|
+
return typeof text === "string" && text.trim().length > 0
|
|
23
|
+
? { content: [{ type: "text" as const, text }] }
|
|
24
|
+
: jsonResult(parsed.value);
|
|
25
|
+
}
|
|
5
26
|
|
|
6
27
|
const args = {
|
|
7
|
-
|
|
28
|
+
mode: z.enum(["content", "navigate", "lookup"]).default("content").describe(
|
|
29
|
+
"`content` (default) reads a record's full text by `recordId`. "
|
|
30
|
+
+ "`lookup` resolves a URL / issue key / external ID to a recordId. "
|
|
31
|
+
+ "`navigate` browses the knowledge graph tree.",
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
// mode: "content"
|
|
35
|
+
recordId: z.string().min(1).optional().describe(
|
|
8
36
|
"Record identifier — usually a UUID for connector-sourced records or "
|
|
9
37
|
+ "a 24-character ObjectId for uploaded records. Get it from a chat "
|
|
10
|
-
+ "citation (`citations[*].recordId`) or from a `pipeshub_search` hit."
|
|
38
|
+
+ "citation (`citations[*].recordId`) or from a `pipeshub_search` hit. "
|
|
39
|
+
+ "Required when `mode` is `content`.",
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
// mode: "navigate"
|
|
43
|
+
nodeId: z.string().min(1).max(2048).optional().describe(
|
|
44
|
+
"The node to open. Take it from a `record_id=` or `node_id=` shown in a "
|
|
45
|
+
+ "previous navigate or lookup response, from a search hit's `recordId`, "
|
|
46
|
+
+ "or from a `pipeshub_sources` id — a KB or connector id opens that "
|
|
47
|
+
+ "source directly. Omit it entirely for the flat listing of everything "
|
|
48
|
+
+ "reachable, newest first — the usual starting point. A URL or an issue "
|
|
49
|
+
+ "key such as `PA-1787` also works: it is resolved to its record "
|
|
50
|
+
+ "automatically, so no separate lookup is needed.",
|
|
51
|
+
),
|
|
52
|
+
page: z.int().min(1).default(1).describe("Page number, 1-indexed."),
|
|
53
|
+
limit: z.int().min(50).max(200).default(50).describe(
|
|
54
|
+
"Children per page. The minimum is 50 — smaller values are rejected "
|
|
55
|
+
+ "rather than silently raised.",
|
|
56
|
+
),
|
|
57
|
+
depth: z.int().min(1).max(3).default(1).describe(
|
|
58
|
+
"Levels of descendants to return in one call. Above 1, the listing is a "
|
|
59
|
+
+ "flat list of all descendants down to that level rather than only "
|
|
60
|
+
+ "direct children, and each row carries its own `level`.",
|
|
61
|
+
),
|
|
62
|
+
nodeTypes: z.array(z.string()).optional().describe(
|
|
63
|
+
"Restrict children to these node types, e.g. `[\"record\", \"folder\"]`.",
|
|
64
|
+
),
|
|
65
|
+
createdAfter: z.string().optional().describe(
|
|
66
|
+
"Filter children by source creation time. ISO 8601 `YYYY-MM-DD`, or a "
|
|
67
|
+
+ "full datetime that MUST carry a timezone offset — a naive datetime is "
|
|
68
|
+
+ "rejected rather than assumed to be UTC.",
|
|
69
|
+
),
|
|
70
|
+
createdBefore: z.string().optional().describe(
|
|
71
|
+
"Filter children by source creation time. `YYYY-MM-DD` is inclusive of "
|
|
72
|
+
+ "the whole day.",
|
|
73
|
+
),
|
|
74
|
+
modifiedAfter: z.string().optional().describe(
|
|
75
|
+
"Filter children by source modification time. Same formats as "
|
|
76
|
+
+ "`createdAfter`.",
|
|
77
|
+
),
|
|
78
|
+
modifiedBefore: z.string().optional().describe(
|
|
79
|
+
"Filter children by source modification time. Same formats as "
|
|
80
|
+
+ "`createdBefore`.",
|
|
81
|
+
),
|
|
82
|
+
|
|
83
|
+
// mode: "lookup"
|
|
84
|
+
identifiers: z.union([
|
|
85
|
+
z.string().min(1).max(2048),
|
|
86
|
+
z.array(z.string().min(1).max(2048)).min(1).max(10),
|
|
87
|
+
]).optional().describe(
|
|
88
|
+
"The reference(s) to resolve: a URL, an issue key such as `PA-1787`, or "
|
|
89
|
+
+ "a bare external system ID. Paste each exactly as you found it — "
|
|
90
|
+
+ "tracking parameters and fragments are handled. Pass a single string, "
|
|
91
|
+
+ "or an array of up to 10 to resolve them in one call. Required when "
|
|
92
|
+
+ "`mode` is `lookup`.",
|
|
93
|
+
),
|
|
94
|
+
connectorName: z.string().optional().describe(
|
|
95
|
+
"Optional hint that prioritises resolution order, e.g. `JIRA`, "
|
|
96
|
+
+ "`CONFLUENCE`, `GOOGLE_DRIVE`, `SLACK`. It cannot widen the search beyond the "
|
|
97
|
+
+ "connectors you can already access. Useful on a retry when a lookup "
|
|
98
|
+
+ "came back empty.",
|
|
11
99
|
),
|
|
12
100
|
};
|
|
13
101
|
|
|
14
102
|
export const tool$pipeshubGetRecordContent: ToolDefinition<typeof args> = {
|
|
15
103
|
name: "pipeshub_get_record_content",
|
|
16
104
|
description:
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
105
|
+
`Three operations on the org's records. Pick by what you hold:
|
|
106
|
+
|
|
107
|
+
\`mode:"lookup"\` — a URL, issue key (PA-1787), or external ID
|
|
108
|
+
→ its recordId plus the record's metadata
|
|
109
|
+
\`mode:"navigate"\` — a question about structure: what is under X,
|
|
110
|
+
what links to Y → browses the hierarchy
|
|
111
|
+
\`mode:"content"\` — a recordId, and you need the document's COMPLETE text
|
|
112
|
+
|
|
113
|
+
**\`mode:"content"\` (default)** — the only way to see a document's complete
|
|
114
|
+
text. Use it whenever missing part of the document could make the answer
|
|
115
|
+
wrong: summarize, extract or list ALL of something, check whether or where
|
|
116
|
+
a doc mentions X, review, or compare named docs. \`pipeshub_chat\` cannot do
|
|
117
|
+
these — it never sees a whole document.
|
|
118
|
+
|
|
119
|
+
Judge by the user's INTENT, not their keywords: "what's this doc about?",
|
|
120
|
+
"walk me through the report", "anything in here about Y?" are all
|
|
121
|
+
full-content tasks. Get the \`recordId\` from a \`pipeshub_search\` top hit, a
|
|
122
|
+
chat citation, or \`mode:"lookup"\`.
|
|
123
|
+
|
|
124
|
+
Returns one \`content\` string: a metadata header (title, source, key fields,
|
|
125
|
+
pre-generated summary) then the full parsed text. A record with no
|
|
126
|
+
extractable content returns the literal \`No record found\`. Use
|
|
127
|
+
\`pipeshub_download_record\` only for the original file bytes.
|
|
128
|
+
|
|
129
|
+
**\`mode:"navigate"\`** — browse the hierarchy: RecordGroup (project /
|
|
130
|
+
space / drive / folder) → Record (epic / story / page / file) → children,
|
|
131
|
+
with breadcrumbs, related links and record IDs.
|
|
132
|
+
|
|
133
|
+
Use it when the question depends on structure rather than wording: what is
|
|
134
|
+
under this epic, which pages sit in this space, what is linked to this
|
|
135
|
+
ticket, what is in this folder — and every "how many" / "all of" / "every"
|
|
136
|
+
question. Search ranks by content; only this shows how records relate, and
|
|
137
|
+
only this gives a count you can trust.
|
|
138
|
+
|
|
139
|
+
Omit \`nodeId\` for a flat listing of everything reachable, most recently
|
|
140
|
+
updated first — the usual starting point. A URL, an issue key, or a
|
|
141
|
+
\`pipeshub_sources\` id also works and resolves automatically.
|
|
142
|
+
|
|
143
|
+
Pass \`depth:2\` or \`depth:3\` to see several levels in ONE call — an epic's
|
|
144
|
+
stories AND their subtasks, a space's pages AND their children — instead of
|
|
145
|
+
one call per level. Use it whenever the question needs an overview of a
|
|
146
|
+
hierarchy rather than a single node.
|
|
147
|
+
|
|
148
|
+
Opening a record also prints that record's own metadata — for a ticket,
|
|
149
|
+
status, assignee, priority and dates — so a question about one record is
|
|
150
|
+
often answered by this call alone. It returns no document text; for that,
|
|
151
|
+
re-call with \`mode:"content"\`.
|
|
152
|
+
|
|
153
|
+
Returns \`Path\` breadcrumbs, the current node's metadata, a children listing
|
|
154
|
+
carrying \`record_id=\` or \`node_id=\` per row plus the group's total
|
|
155
|
+
(\`Children 1-50 of 61\`), \`Related\` cross-references, and a \`Next:\` line.
|
|
156
|
+
One page is usually every child, so only pass \`page:2\` when that \`Next:\`
|
|
157
|
+
line says more exist.
|
|
158
|
+
|
|
159
|
+
**\`mode:"lookup"\`** — turn an external reference into a recordId, the first
|
|
160
|
+
step whenever the question names one. Returns that record's metadata (for a
|
|
161
|
+
ticket: status, assignee, priority, dates) plus its recordId, which
|
|
162
|
+
\`mode:"navigate"\` takes to list what is under it and \`mode:"content"\` takes
|
|
163
|
+
to read it.
|
|
164
|
+
|
|
165
|
+
Handles Jira keys and URLs, Confluence, Drive, Slack permalinks, Linear,
|
|
166
|
+
Notion, ServiceNow sys_id, SharePoint, Gmail/Outlook, and any connector
|
|
167
|
+
whose records index a web URL. Resolution searches ALL connectors you can
|
|
168
|
+
access, regardless of any source filter you used elsewhere.
|
|
169
|
+
|
|
170
|
+
A miss is a 200 with empty \`matches\` and the input echoed in
|
|
171
|
+
\`not_found_identifiers\` — that may mean no-access, not non-existence. Use
|
|
172
|
+
\`mode:"navigate"\` to confirm the record exists before telling the user it
|
|
173
|
+
does not. If \`ambiguous\` is true, pick from \`matches\` rather than taking
|
|
174
|
+
the first.
|
|
175
|
+
|
|
176
|
+
Navigate and lookup return a rendered text view whose closing \`Next:\` line
|
|
177
|
+
names the exact follow-up call — follow it. When presenting a record, link
|
|
178
|
+
it using the \`Web URL\` from its metadata header (when present).`,
|
|
41
179
|
scopes: ["read"],
|
|
42
180
|
annotations: {
|
|
43
|
-
title: "
|
|
181
|
+
title: "Read, browse, or resolve records",
|
|
44
182
|
destructiveHint: false,
|
|
45
183
|
idempotentHint: true,
|
|
46
184
|
openWorldHint: false,
|
|
@@ -48,9 +186,55 @@ you need the original file bytes.`,
|
|
|
48
186
|
},
|
|
49
187
|
args,
|
|
50
188
|
tool: async (client, args, ctx) => {
|
|
189
|
+
const fetchOptions = { fetchOptions: { signal: ctx.signal } };
|
|
190
|
+
|
|
191
|
+
// Navigate / lookup return the rendered `text` view rather than the
|
|
192
|
+
// `{ content: <string> }` envelope that content mode unwraps.
|
|
193
|
+
if (args.mode === "navigate") {
|
|
194
|
+
const [r] = await connectorNavigateKnowledgeGraph(client, {
|
|
195
|
+
nodeId: args.nodeId,
|
|
196
|
+
page: args.page,
|
|
197
|
+
limit: args.limit,
|
|
198
|
+
depth: args.depth,
|
|
199
|
+
nodeTypes: args.nodeTypes,
|
|
200
|
+
createdAfter: args.createdAfter,
|
|
201
|
+
createdBefore: args.createdBefore,
|
|
202
|
+
modifiedAfter: args.modifiedAfter,
|
|
203
|
+
modifiedBefore: args.modifiedBefore,
|
|
204
|
+
}, fetchOptions).$inspect();
|
|
205
|
+
if (!r.ok) return errorResult(r.error.message);
|
|
206
|
+
|
|
207
|
+
const httpErr = await httpErrorResult(r.value, "Navigate knowledge graph");
|
|
208
|
+
if (httpErr) return httpErr;
|
|
209
|
+
|
|
210
|
+
return renderedResult(r.value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (args.mode === "lookup") {
|
|
214
|
+
const identifiers = typeof args.identifiers === "string"
|
|
215
|
+
? [args.identifiers]
|
|
216
|
+
: args.identifiers ?? [];
|
|
217
|
+
if (identifiers.length === 0) {
|
|
218
|
+
return errorResult('`identifiers` is required when mode is "lookup".');
|
|
219
|
+
}
|
|
220
|
+
const [r] = await connectorLookupRecordByIdentifier(client, {
|
|
221
|
+
identifiers,
|
|
222
|
+
connectorName: args.connectorName,
|
|
223
|
+
}, fetchOptions).$inspect();
|
|
224
|
+
if (!r.ok) return errorResult(r.error.message);
|
|
225
|
+
|
|
226
|
+
const httpErr = await httpErrorResult(r.value, "Record lookup");
|
|
227
|
+
if (httpErr) return httpErr;
|
|
228
|
+
|
|
229
|
+
return renderedResult(r.value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!args.recordId) {
|
|
233
|
+
return errorResult('`recordId` is required when mode is "content".');
|
|
234
|
+
}
|
|
51
235
|
const [result] = await connectorGetRecordContent(client, {
|
|
52
236
|
recordId: args.recordId,
|
|
53
|
-
},
|
|
237
|
+
}, fetchOptions).$inspect();
|
|
54
238
|
if (!result.ok) return errorResult(result.error.message);
|
|
55
239
|
|
|
56
240
|
// The SDK func uses errorCodes:[], so any non-2xx comes back as an
|
|
@@ -38,10 +38,23 @@ Typical uses:
|
|
|
38
38
|
- Show the user a ranked list of matching files when they ask "find /
|
|
39
39
|
search for X".
|
|
40
40
|
|
|
41
|
+
Not for structural questions — what is under this epic, which pages are
|
|
42
|
+
in this space, what links to this ticket. Ranking by content cannot show
|
|
43
|
+
how records relate; use \`pipeshub_get_record_content\` \`mode:"navigate"\`.
|
|
44
|
+
|
|
45
|
+
**A ranked sample, never a complete list.** Hits are the top-scoring
|
|
46
|
+
blocks from the best-matching records — not all blocks of any record, and
|
|
47
|
+
not every record that matches. Never count them to answer "how many" /
|
|
48
|
+
"all" / "every"; navigate the record group instead, which reports its
|
|
49
|
+
real total.
|
|
50
|
+
|
|
41
51
|
The response is trimmed to one row per hit:
|
|
42
52
|
\`{ recordId, recordName, score, snippet, mimeType, webUrl, ... }\`.
|
|
43
53
|
Highest \`score\` first; multiple hits may share the same \`recordId\`
|
|
44
|
-
(different
|
|
54
|
+
(different blocks of the same record).
|
|
55
|
+
|
|
56
|
+
When presenting results to the user, link each record using its
|
|
57
|
+
\`webUrl\` (when present).`,
|
|
45
58
|
scopes: ["read"],
|
|
46
59
|
annotations: {
|
|
47
60
|
title: "Semantic search",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from Speakeasy-generated lookuprecordbyidentifierop.
|
|
3
|
+
*
|
|
4
|
+
* Request only. The generated file also declared a `LookupRecordByIdentifierResponse`
|
|
5
|
+
* union, which pulls in further response models; the func returns a raw `Response`
|
|
6
|
+
* (errorCodes: []) and the tool parses it with `readJson`, so none of that is used
|
|
7
|
+
* here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as z from "zod";
|
|
11
|
+
|
|
12
|
+
export type LookupRecordByIdentifierRequest = {
|
|
13
|
+
identifiers: Array<string>;
|
|
14
|
+
connectorName?: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LookupRecordByIdentifierRequest$zodSchema: z.ZodType<
|
|
18
|
+
LookupRecordByIdentifierRequest
|
|
19
|
+
> = z.object({
|
|
20
|
+
connectorName: z.string().describe(
|
|
21
|
+
"Connector hint to prioritise resolution order, e.g. `JIRA`, `CONFLUENCE`, `GOOGLE_DRIVE`, `SLACK`. Cannot widen beyond the connectors the caller can access.",
|
|
22
|
+
).optional(),
|
|
23
|
+
identifiers: z.array(z.string().min(1).max(2048)).min(1).max(10).describe(
|
|
24
|
+
"The references to resolve: URLs, issue keys such as `PA-1787`, or bare external system IDs. Paste each exactly as you found it — tracking parameters and fragments are handled. Up to 10 per call.",
|
|
25
|
+
),
|
|
26
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from Speakeasy-generated navigateknowledgegraphop.
|
|
3
|
+
*
|
|
4
|
+
* Request only. The generated file also declared a `NavigateKnowledgeGraphResponse`
|
|
5
|
+
* union, which pulls in ~10 further response models; the func returns a raw
|
|
6
|
+
* `Response` (errorCodes: []) and the tool parses it with `readJson`, so none of
|
|
7
|
+
* that is used here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as z from "zod";
|
|
11
|
+
|
|
12
|
+
export type NavigateKnowledgeGraphRequest = {
|
|
13
|
+
nodeId?: string | undefined;
|
|
14
|
+
page?: number | undefined;
|
|
15
|
+
limit?: number | undefined;
|
|
16
|
+
depth?: number | undefined;
|
|
17
|
+
nodeTypes?: Array<string> | undefined;
|
|
18
|
+
createdAfter?: string | undefined;
|
|
19
|
+
createdBefore?: string | undefined;
|
|
20
|
+
modifiedAfter?: string | undefined;
|
|
21
|
+
modifiedBefore?: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Bounds come from the OpenAPI spec rather than the generated schema, which
|
|
25
|
+
// dropped them: the endpoint 400s on `limit` outside 50-200, `depth` outside
|
|
26
|
+
// 1-3, `page` below 1, or an empty `nodeId`. Rejecting client-side turns each
|
|
27
|
+
// of those into a validation message instead of a round-trip.
|
|
28
|
+
export const NavigateKnowledgeGraphRequest$zodSchema: z.ZodType<
|
|
29
|
+
NavigateKnowledgeGraphRequest
|
|
30
|
+
> = z.object({
|
|
31
|
+
createdAfter: z.string().describe(
|
|
32
|
+
"Filter children by source creation time. ISO 8601 `YYYY-MM-DD`, or a full datetime that MUST carry a timezone offset — a naive datetime is rejected rather than assumed to be UTC.",
|
|
33
|
+
).optional(),
|
|
34
|
+
createdBefore: z.string().describe(
|
|
35
|
+
"Filter children by source creation time. `YYYY-MM-DD` is inclusive of the whole day.",
|
|
36
|
+
).optional(),
|
|
37
|
+
depth: z.int().min(1).max(3).describe(
|
|
38
|
+
"Levels of descendants to return in one call. Above 1, `rows` is a flat list of all descendants down to that level rather than only direct children, and each row carries its own `level`.",
|
|
39
|
+
).optional(),
|
|
40
|
+
limit: z.int().min(50).max(200).describe(
|
|
41
|
+
"Children per page. The minimum is 50 — smaller values are rejected rather than silently raised.",
|
|
42
|
+
).optional(),
|
|
43
|
+
modifiedAfter: z.string().describe(
|
|
44
|
+
"Filter children by source modification time.",
|
|
45
|
+
).optional(),
|
|
46
|
+
modifiedBefore: z.string().describe(
|
|
47
|
+
"Filter children by source modification time.",
|
|
48
|
+
).optional(),
|
|
49
|
+
nodeId: z.string().min(1).max(2048).describe(
|
|
50
|
+
"The node to open. Take it from an `id` in a previous navigate or lookup response. Omit it entirely for the flat listing of everything reachable — the usual starting point. A URL or an issue key such as `PA-1787` also works: it is resolved to its record automatically.",
|
|
51
|
+
).optional(),
|
|
52
|
+
nodeTypes: z.array(z.string()).describe(
|
|
53
|
+
"Restrict children to these node types. Repeat the parameter for multiple types: `?nodeTypes=record&nodeTypes=folder`.",
|
|
54
|
+
).optional(),
|
|
55
|
+
page: z.int().min(1).describe("Page number, 1-indexed.").optional(),
|
|
56
|
+
});
|