@pipeshub-ai/mcp 2.1.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 +92 -2
- package/bin/mcp-server.js +976 -169
- package/bin/mcp-server.js.map +28 -15
- 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/connectorGetRecordContent.d.ts +43 -0
- package/esm/funcs/connectorGetRecordContent.d.ts.map +1 -0
- package/esm/funcs/connectorGetRecordContent.js +114 -0
- package/esm/funcs/connectorGetRecordContent.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/funcs/conversationsStreamConversation.js +1 -1
- package/esm/funcs/conversationsStreamConversation.js.map +1 -1
- package/esm/funcs/userGroupsGetAllUserGroups.js +1 -1
- package/esm/funcs/userGroupsGetAllUserGroups.js.map +1 -1
- package/esm/hooks/registration.d.ts.map +1 -1
- package/esm/hooks/registration.js +2 -1
- package/esm/hooks/registration.js.map +1 -1
- package/esm/hooks/request-context.d.ts +10 -0
- package/esm/hooks/request-context.d.ts.map +1 -0
- package/esm/hooks/request-context.js +38 -0
- package/esm/hooks/request-context.js.map +1 -0
- package/esm/hooks/requestid.d.ts +5 -0
- package/esm/hooks/requestid.d.ts.map +1 -0
- package/esm/hooks/requestid.js +58 -0
- package/esm/hooks/requestid.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 +62 -9
- package/esm/mcp-server/instructions.js.map +1 -1
- package/esm/mcp-server/server.d.ts.map +1 -1
- package/esm/mcp-server/server.js +10 -3
- package/esm/mcp-server/server.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 -92
- package/esm/mcp-server/tools/pipeshubChat.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts +125 -0
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts.map +1 -0
- package/esm/mcp-server/tools/pipeshubGetRecordContent.js +214 -0
- package/esm/mcp-server/tools/pipeshubGetRecordContent.js.map +1 -0
- package/esm/mcp-server/tools/pipeshubSearch.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubSearch.js +22 -5
- package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
- package/esm/mcp-server/tools.d.ts.map +1 -1
- package/esm/mcp-server/tools.js +13 -1
- package/esm/mcp-server/tools.js.map +1 -1
- package/esm/models/availablemodelsresponse.d.ts +1 -1
- package/esm/models/conversation.d.ts +1 -1
- package/esm/models/getrecordcontentop.d.ts +6 -0
- package/esm/models/getrecordcontentop.d.ts.map +1 -0
- package/esm/models/getrecordcontentop.js +5 -0
- package/esm/models/getrecordcontentop.js.map +1 -0
- 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.d.ts.map +1 -1
- package/esm/tool-names.js +6 -2
- package/esm/tool-names.js.map +1 -1
- package/package.json +4 -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/connectorGetRecordContent.ts +179 -0
- package/src/funcs/connectorLookupRecordByIdentifier.ts +200 -0
- package/src/funcs/connectorNavigateKnowledgeGraph.ts +215 -0
- package/src/funcs/conversationsStreamConversation.ts +1 -1
- package/src/funcs/userGroupsGetAllUserGroups.ts +1 -1
- package/src/hooks/registration.ts +2 -1
- package/src/hooks/request-context.ts +47 -0
- package/src/hooks/requestid.ts +62 -0
- package/src/lib/base64.ts +6 -3
- package/src/lib/bytes-to-base64.ts +24 -0
- package/src/mcp-server/instructions.ts +62 -9
- package/src/mcp-server/server.ts +11 -3
- 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 -91
- package/src/mcp-server/tools/pipeshubGetRecordContent.ts +251 -0
- package/src/mcp-server/tools/pipeshubSearch.ts +22 -5
- package/src/mcp-server/tools.ts +14 -1
- package/src/models/getrecordcontentop.ts +11 -0
- package/src/models/lookuprecordbyidentifierop.ts +26 -0
- package/src/models/navigateknowledgegraphop.ts +56 -0
- package/src/tool-names.ts +6 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Stamp an x-request-id trace header onto every outbound SDK request, reusing
|
|
3
|
+
* the id bound for the current MCP tool/resource/prompt call (see
|
|
4
|
+
* request-context.ts) so all backend calls from one invocation share it.
|
|
5
|
+
*
|
|
6
|
+
* Format mirrors the TypeScript SDK's own RequestIDHook: `mcp-<userId>-<random>`
|
|
7
|
+
* when the bearer JWT carries a userId claim, else `mcp-<random>`. The userId
|
|
8
|
+
* is only knowable once a request actually carries a resolved Authorization
|
|
9
|
+
* header, so it's decoded off the first outbound request of the tool call and
|
|
10
|
+
* cached on the bound context for every subsequent call to reuse verbatim.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getRequestContextStore, newRequestId } from "./request-context.js";
|
|
14
|
+
import { BeforeRequestContext, BeforeRequestHook } from "./types.js";
|
|
15
|
+
|
|
16
|
+
const REQUEST_ID_HEADER = "x-request-id";
|
|
17
|
+
|
|
18
|
+
/** Decodes the JWT payload without verifying the signature. */
|
|
19
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
20
|
+
try {
|
|
21
|
+
const base64Url = token.split(".")[1];
|
|
22
|
+
if (!base64Url) return null;
|
|
23
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
24
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
|
|
25
|
+
const claims = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
26
|
+
return typeof claims === "object" && claims !== null ? claims : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function userIdFromRequest(request: Request): string | null {
|
|
33
|
+
const auth = request.headers.get("authorization") ?? "";
|
|
34
|
+
if (!auth.toLowerCase().startsWith("bearer ")) return null;
|
|
35
|
+
const token = auth.slice(7).trim();
|
|
36
|
+
const claims = decodeJwtPayload(token);
|
|
37
|
+
return (claims?.["userId"] as string) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class RequestIDHook implements BeforeRequestHook {
|
|
41
|
+
beforeRequest(_hookCtx: BeforeRequestContext, request: Request): Request {
|
|
42
|
+
if (request.headers.get(REQUEST_ID_HEADER)) {
|
|
43
|
+
return request;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const store = getRequestContextStore();
|
|
47
|
+
if (!store) {
|
|
48
|
+
// No bound tool-call context (shouldn't happen in practice).
|
|
49
|
+
request.headers.set(REQUEST_ID_HEADER, newRequestId());
|
|
50
|
+
return request;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!store.finalId) {
|
|
54
|
+
const userId = userIdFromRequest(request);
|
|
55
|
+
store.finalId = userId
|
|
56
|
+
? `mcp-${userId}-${store.random}`
|
|
57
|
+
: `mcp-${store.random}`;
|
|
58
|
+
}
|
|
59
|
+
request.headers.set(REQUEST_ID_HEADER, store.finalId);
|
|
60
|
+
return request;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/lib/base64.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
|
|
3
|
+
*
|
|
4
|
+
* Exception: `bytesToBase64` is re-exported from the hand-maintained
|
|
5
|
+
* `./bytes-to-base64.js` (see that file and `.genignore`). Do not restore
|
|
6
|
+
* the generated one-liner — it overflows the stack on binary payloads.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import * as z from "zod";
|
|
6
10
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
}
|
|
11
|
+
export { bytesToBase64 } from "./bytes-to-base64.js";
|
|
12
|
+
import { bytesToBase64 } from "./bytes-to-base64.js";
|
|
10
13
|
|
|
11
14
|
export function bytesFromBase64(encoded: string): Uint8Array {
|
|
12
15
|
return Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-maintained base64 encoder for binary payloads.
|
|
3
|
+
*
|
|
4
|
+
* Speakeasy's generated helper is:
|
|
5
|
+
* btoa(String.fromCodePoint(...u8arr))
|
|
6
|
+
* which passes one argument per byte and throws "Maximum call stack size
|
|
7
|
+
* exceeded" once the array is larger than the engine's argument limit
|
|
8
|
+
* (between 64 KB and 128 KB on Node 24). That is smaller than a typical
|
|
9
|
+
* PDF or image, so binary MCP results fail outright.
|
|
10
|
+
*
|
|
11
|
+
* Keep this file out of Speakeasy regeneration (see `.genignore`). The
|
|
12
|
+
* generated wrappers in `src/lib/base64.ts` and `src/mcp-server/shared.ts`
|
|
13
|
+
* re-export / call through here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BASE64_CHUNK = 0x8000;
|
|
17
|
+
|
|
18
|
+
export function bytesToBase64(u8arr: Uint8Array): string {
|
|
19
|
+
let binary = "";
|
|
20
|
+
for (let i = 0; i < u8arr.length; i += BASE64_CHUNK) {
|
|
21
|
+
binary += String.fromCharCode(...u8arr.subarray(i, i + BASE64_CHUNK));
|
|
22
|
+
}
|
|
23
|
+
return btoa(binary);
|
|
24
|
+
}
|
|
@@ -9,14 +9,35 @@ PipesHub is the user's workplace AI platform. It indexes their documents,
|
|
|
9
9
|
knowledge base content, and connector sources (Drive, Box, Confluence,
|
|
10
10
|
Slack, Jira, Gmail, ...). When in doubt, the answer is in PipesHub.
|
|
11
11
|
|
|
12
|
+
## Full-document tasks: \`pipeshub_search\` → \`pipeshub_get_record_content\`
|
|
13
|
+
|
|
14
|
+
\`pipeshub_chat\` answers from a handful of retrieved passages — it never
|
|
15
|
+
reads a whole document. Whenever the task depends on a document's
|
|
16
|
+
COMPLETE content, fetch the document itself:
|
|
17
|
+
|
|
18
|
+
1. \`pipeshub_search\` with the document's name / topic.
|
|
19
|
+
2. Take the top hit's \`recordId\`.
|
|
20
|
+
3. \`pipeshub_get_record_content\`, and answer from the returned content.
|
|
21
|
+
|
|
22
|
+
Tasks that need this path — anything where missing part of the document
|
|
23
|
+
could make the answer wrong:
|
|
24
|
+
|
|
25
|
+
- Summarize / TL;DR / key points / takeaways / action items of a doc.
|
|
26
|
+
- Extract or list ALL of something (dates, owners, requirements, ...).
|
|
27
|
+
- Check whether / where a doc mentions something.
|
|
28
|
+
- Translate, rewrite, outline, review, or reformat a doc.
|
|
29
|
+
- Compare named docs (fetch each \`recordId\`).
|
|
30
|
+
- Any question explicitly scoped to ONE named document — chat retrieval
|
|
31
|
+
cannot be restricted to a single record.
|
|
32
|
+
|
|
12
33
|
## Default tool: \`pipeshub_chat\`
|
|
13
34
|
|
|
14
35
|
**Use \`pipeshub_chat\` for any question that could plausibly be answered
|
|
15
|
-
by the user's PipesHub-indexed data
|
|
36
|
+
by the user's PipesHub-indexed data** and is not a full-document task:
|
|
16
37
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
- A question that may span several documents, or where you don't yet
|
|
39
|
+
know which record holds the answer (e.g. "what did Aashil say about
|
|
40
|
+
onboarding?").
|
|
20
41
|
- Anything about company / org policies, processes, decisions, or
|
|
21
42
|
history (e.g. "what's our vacation policy?", "who owns the auth
|
|
22
43
|
service?").
|
|
@@ -32,15 +53,17 @@ the user can verify.
|
|
|
32
53
|
|
|
33
54
|
## When to use the other tools
|
|
34
55
|
|
|
35
|
-
- \`pipeshub_search\` —
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
the doc say?" use \`pipeshub_chat\` instead — it does the retrieval
|
|
39
|
-
internally.
|
|
56
|
+
- \`pipeshub_search\` — locate a document by name or topic and resolve it
|
|
57
|
+
to a \`recordId\`. To read or summarize one specific document, search,
|
|
58
|
+
then pass the top hit's \`recordId\` to \`pipeshub_get_record_content\`.
|
|
40
59
|
- \`pipeshub_download_record\` — when the user wants the actual file
|
|
41
60
|
bytes (download, attach, open). Get the \`recordId\` either from
|
|
42
61
|
citations on a prior \`pipeshub_chat\` response or from
|
|
43
62
|
\`pipeshub_search\`.
|
|
63
|
+
- \`pipeshub_get_record_content\` — when you need a record's full parsed
|
|
64
|
+
content (returned as a single \`content\` string: metadata header plus
|
|
65
|
+
the document's text) without downloading the original file. Prefer this
|
|
66
|
+
over download when the question is about what the record says.
|
|
44
67
|
- \`pipeshub_directory\` — people, groups, teams, and \`whoami\` lookups.
|
|
45
68
|
Not for documents.
|
|
46
69
|
- \`pipeshub_sources\` — call once at the start of a session to discover
|
|
@@ -48,6 +71,36 @@ the user can verify.
|
|
|
48
71
|
- \`pipeshub_agents\` — list the org's configured **agents** (specialized
|
|
49
72
|
assistants with their own prompt, tools, and knowledge scope).
|
|
50
73
|
|
|
74
|
+
## Knowledge Sources
|
|
75
|
+
|
|
76
|
+
A source (an app connector or a KB) holds record groups (a project, space,
|
|
77
|
+
drive, folder), which hold records (one ticket, page, file). Records nest —
|
|
78
|
+
an epic over its stories, a page over its child pages.
|
|
79
|
+
|
|
80
|
+
**Every tool returns metadata or content.**
|
|
81
|
+
|
|
82
|
+
- \`pipeshub_search\` / \`pipeshub_chat\` — content, but only a ranked sample of
|
|
83
|
+
blocks: never all blocks of a record, never every record that matches, never
|
|
84
|
+
a complete list of what exists.
|
|
85
|
+
- \`mode:"lookup"\` and \`mode:"navigate"\` — metadata: recordIds, fields, and
|
|
86
|
+
structure. No document text. \`mode:"content"\` — one whole record.
|
|
87
|
+
|
|
88
|
+
**Combining them.** Search finds a starting point by wording; navigate on a
|
|
89
|
+
\`recordId\` reveals what search structurally cannot — siblings, children,
|
|
90
|
+
linked records, and the group's total count. For a question that must be
|
|
91
|
+
exhaustive ("all", "how many", "every"), do NOT count search hits or trust a
|
|
92
|
+
chat answer: navigate the record group, which reports the real total. For a
|
|
93
|
+
record's siblings, navigate its parent — take the id from the \`Path\` line.
|
|
94
|
+
|
|
95
|
+
**A named record** — an issue key, a Confluence / Drive / Slack link, an
|
|
96
|
+
external ID → \`mode:"lookup"\` FIRST. It returns that record's metadata (for a
|
|
97
|
+
ticket: status, assignee, priority, dates) plus its \`recordId\`, which often
|
|
98
|
+
answers the question outright. Never guess a \`recordId\`.
|
|
99
|
+
|
|
100
|
+
Pass a source's id from \`pipeshub_sources\` as \`apps: ["<id>"]\` to search or
|
|
101
|
+
chat, and as \`nodeId\` to navigate. Navigate and lookup end in a \`Next:\` line
|
|
102
|
+
naming the exact follow-up call — follow it.
|
|
103
|
+
|
|
51
104
|
## Agents
|
|
52
105
|
|
|
53
106
|
Some orgs configure **agents** for specific jobs (e.g. a Slack messenger, a
|
package/src/mcp-server/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
|
|
3
3
|
import { PIPESHUB_INSTRUCTIONS } from "./instructions.js";
|
|
4
4
|
import { PipeshubCore } from "../core.js";
|
|
5
|
+
import { bindNewRequestId } from "../hooks/request-context.js";
|
|
5
6
|
import { SDKOptions } from "../lib/config.js";
|
|
6
7
|
import type { ConsoleLogger } from "./console-logger.js";
|
|
7
8
|
import { createRegisterPrompt } from "./prompts.js";
|
|
@@ -14,6 +15,7 @@ import { createRegisterTool, registerDynamicTools } from "./tools.js";
|
|
|
14
15
|
import { tool$pipeshubChat } from "./tools/pipeshubChat.js";
|
|
15
16
|
import { tool$pipeshubSearch } from "./tools/pipeshubSearch.js";
|
|
16
17
|
import { tool$pipeshubDownloadRecord } from "./tools/pipeshubDownloadRecord.js";
|
|
18
|
+
import { tool$pipeshubGetRecordContent } from "./tools/pipeshubGetRecordContent.js";
|
|
17
19
|
import { tool$pipeshubDirectory } from "./tools/pipeshubDirectory.js";
|
|
18
20
|
import { tool$pipeshubSources } from "./tools/pipeshubSources.js";
|
|
19
21
|
import { tool$pipeshubAgents } from "./tools/pipeshubAgents.js";
|
|
@@ -40,7 +42,7 @@ export function createMCPServer(deps: {
|
|
|
40
42
|
},
|
|
41
43
|
);
|
|
42
44
|
|
|
43
|
-
const
|
|
45
|
+
const resolveClient = deps.getSDK || (() =>
|
|
44
46
|
new PipeshubCore({
|
|
45
47
|
security: deps.security,
|
|
46
48
|
serverURL: deps.serverURL,
|
|
@@ -55,6 +57,11 @@ export function createMCPServer(deps: {
|
|
|
55
57
|
: undefined,
|
|
56
58
|
}));
|
|
57
59
|
|
|
60
|
+
const getClient = () => {
|
|
61
|
+
bindNewRequestId();
|
|
62
|
+
return resolveClient();
|
|
63
|
+
};
|
|
64
|
+
|
|
58
65
|
const scopes = new Set(deps.scopes);
|
|
59
66
|
|
|
60
67
|
const allowedTools = deps.allowedTools && new Set(deps.allowedTools);
|
|
@@ -87,8 +94,9 @@ export function createMCPServer(deps: {
|
|
|
87
94
|
tool(tool$pipeshubChat); // 2. ask questions (start + continue)
|
|
88
95
|
tool(tool$pipeshubSearch); // 3. resolve filename → recordId
|
|
89
96
|
tool(tool$pipeshubDownloadRecord); // 4. fetch a document by id
|
|
90
|
-
tool(tool$
|
|
91
|
-
tool(tool$
|
|
97
|
+
tool(tool$pipeshubGetRecordContent); // 5. fetch a record's parsed content
|
|
98
|
+
tool(tool$pipeshubDirectory); // 6. people / groups / teams / whoami
|
|
99
|
+
tool(tool$pipeshubAgents); // 7. discover org agents
|
|
92
100
|
|
|
93
101
|
// Curated prompt: user-invokable tool-routing guidance.
|
|
94
102
|
prompt(prompt$pipeshubAssistant);
|
package/src/mcp-server/shared.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
|
|
3
|
+
*
|
|
4
|
+
* Exception: `bytesToBase64` is imported from the hand-maintained
|
|
5
|
+
* `../lib/bytes-to-base64.js` (see that file and `.genignore`). Do not
|
|
6
|
+
* restore the generated one-liner — it overflows the stack on binary
|
|
7
|
+
* payloads.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import * as z from "zod";
|
|
11
|
+
import { bytesToBase64 } from "../lib/bytes-to-base64.js";
|
|
6
12
|
|
|
7
13
|
type BinaryData =
|
|
8
14
|
| Uint8Array
|
|
@@ -14,9 +20,7 @@ type BinaryData =
|
|
|
14
20
|
|
|
15
21
|
const base64Schema = z.string().base64();
|
|
16
22
|
|
|
17
|
-
export
|
|
18
|
-
return btoa(String.fromCodePoint(...u8arr));
|
|
19
|
-
}
|
|
23
|
+
export { bytesToBase64 };
|
|
20
24
|
|
|
21
25
|
export async function consumeStream(
|
|
22
26
|
stream: ReadableStream<Uint8Array>,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// AG-UI frame folding for the chat stream.
|
|
2
|
+
//
|
|
3
|
+
// AG-UI is the only wire protocol the conversation and agent stream routes
|
|
4
|
+
// speak; the legacy `connected` / `answer_chunk` / `complete` / `error`
|
|
5
|
+
// vocabulary was removed with the new agent loop and is never emitted.
|
|
6
|
+
//
|
|
7
|
+
// Kept separate from `pipeshubChat.ts` so the fold is a pure function over
|
|
8
|
+
// frames and can be exercised without a live stream.
|
|
9
|
+
|
|
10
|
+
import type { SSEFrame } from "./_helpers.js";
|
|
11
|
+
|
|
12
|
+
export const AGUIEvent = {
|
|
13
|
+
RUN_FINISHED: "RUN_FINISHED",
|
|
14
|
+
RUN_ERROR: "RUN_ERROR",
|
|
15
|
+
CUSTOM: "CUSTOM",
|
|
16
|
+
TEXT_MESSAGE_CONTENT: "TEXT_MESSAGE_CONTENT",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type ChatStreamState = {
|
|
20
|
+
/** Persisted conversation from the terminal frame; null until then. */
|
|
21
|
+
conversation: any | null;
|
|
22
|
+
recordsUsed: number | undefined;
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** From `CUSTOM{name:"conversation_created"}`, available before the answer. */
|
|
25
|
+
conversationId: string | null;
|
|
26
|
+
title: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Answer text accumulated per `messageId`. Universal `agent` mode forwards
|
|
29
|
+
* child-run text alongside the root answer, so a single buffer would splice
|
|
30
|
+
* sub-agent output into the middle of the reply.
|
|
31
|
+
*/
|
|
32
|
+
text: Map<string, string>;
|
|
33
|
+
/** messageIds seen on frames with no `parentRunId` — i.e. the root run. */
|
|
34
|
+
rootMessageIds: Set<string>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function newChatStreamState(): ChatStreamState {
|
|
38
|
+
return {
|
|
39
|
+
conversation: null,
|
|
40
|
+
recordsUsed: undefined,
|
|
41
|
+
error: null,
|
|
42
|
+
conversationId: null,
|
|
43
|
+
title: null,
|
|
44
|
+
text: new Map(),
|
|
45
|
+
rootMessageIds: new Set(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fold one frame into `state`. Returns true when the frame is terminal and the
|
|
51
|
+
* caller should stop draining the stream.
|
|
52
|
+
*/
|
|
53
|
+
/** The subset of AG-UI payload fields this fold reads. */
|
|
54
|
+
type FrameData = {
|
|
55
|
+
result?: {
|
|
56
|
+
conversation?: unknown;
|
|
57
|
+
recordsUsed?: number;
|
|
58
|
+
meta?: { recordsUsed?: number };
|
|
59
|
+
};
|
|
60
|
+
message?: string;
|
|
61
|
+
error?: string;
|
|
62
|
+
name?: string;
|
|
63
|
+
value?: { conversationId?: string; title?: string };
|
|
64
|
+
delta?: string;
|
|
65
|
+
messageId?: string;
|
|
66
|
+
parentRunId?: string | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function applyAGUIFrame(
|
|
70
|
+
state: ChatStreamState,
|
|
71
|
+
frame: SSEFrame,
|
|
72
|
+
): boolean {
|
|
73
|
+
const data = frame.data;
|
|
74
|
+
const d: FrameData = data && typeof data === "object"
|
|
75
|
+
? data as FrameData
|
|
76
|
+
: {};
|
|
77
|
+
|
|
78
|
+
switch (frame.event) {
|
|
79
|
+
case AGUIEvent.RUN_FINISHED: {
|
|
80
|
+
// Only the gateway's own re-emitted frame carries `result` — it swallows
|
|
81
|
+
// the AI backend's root RUN_FINISHED and re-sends an enriched one after
|
|
82
|
+
// persisting. Sub-agent RUN_FINISHED frames are forwarded verbatim with
|
|
83
|
+
// no `result`, so keying off the event name alone would end the stream
|
|
84
|
+
// as soon as the first sub-agent finished.
|
|
85
|
+
const result = d.result;
|
|
86
|
+
if (!result || typeof result !== "object") return false;
|
|
87
|
+
state.conversation = result.conversation ?? null;
|
|
88
|
+
state.recordsUsed = result.recordsUsed ?? result.meta?.recordsUsed;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case AGUIEvent.RUN_ERROR: {
|
|
93
|
+
state.error = (typeof data === "string" ? data : null)
|
|
94
|
+
?? d.message ?? d.error ?? frame.raw ?? "Stream error";
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case AGUIEvent.CUSTOM: {
|
|
99
|
+
if (d.name === "conversation_created") {
|
|
100
|
+
const value = d.value ?? {};
|
|
101
|
+
state.conversationId = value.conversationId ?? state.conversationId;
|
|
102
|
+
state.title = value.title ?? state.title;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case AGUIEvent.TEXT_MESSAGE_CONTENT: {
|
|
108
|
+
if (typeof d.delta !== "string" || d.delta.length === 0) return false;
|
|
109
|
+
const id = typeof d.messageId === "string" ? d.messageId : "";
|
|
110
|
+
state.text.set(id, (state.text.get(id) ?? "") + d.delta);
|
|
111
|
+
if (d.parentRunId == null) state.rootMessageIds.add(id);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// RUN_STARTED, STEP_*, TOOL_CALL_*, REASONING_*, STATE_*,
|
|
116
|
+
// TEXT_MESSAGE_START/END, HEARTBEAT — lifecycle noise.
|
|
117
|
+
default:
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Best-effort answer when the stream ended without a terminal frame.
|
|
124
|
+
*
|
|
125
|
+
* Prefers the longest root-run buffer; falls back to the longest buffer of any
|
|
126
|
+
* run so a partial answer still reaches the caller.
|
|
127
|
+
*/
|
|
128
|
+
export function salvagedText(state: ChatStreamState): string | null {
|
|
129
|
+
const pick = (ids: Iterable<string>): string | null => {
|
|
130
|
+
let best: string | null = null;
|
|
131
|
+
for (const id of ids) {
|
|
132
|
+
const text = state.text.get(id);
|
|
133
|
+
if (text && (best === null || text.length > best.length)) best = text;
|
|
134
|
+
}
|
|
135
|
+
return best;
|
|
136
|
+
};
|
|
137
|
+
return pick(state.rootMessageIds) ?? pick(state.text.keys());
|
|
138
|
+
}
|
|
@@ -224,17 +224,24 @@ export function trimConversation(conv: any) {
|
|
|
224
224
|
export function trimCitations(citations: unknown) {
|
|
225
225
|
if (!Array.isArray(citations)) return [];
|
|
226
226
|
return citations.map((c: any) => {
|
|
227
|
-
|
|
227
|
+
// Two wire shapes exist. Flat: `{ content, chunkIndex, metadata }`.
|
|
228
|
+
// Nested (current backend): `{ citationId, citationData: { content,
|
|
229
|
+
// chunkIndex, metadata } }`. Reading only the flat one silently yields
|
|
230
|
+
// a citation with every field `undefined` — which JSON.stringify drops,
|
|
231
|
+
// leaving `{"snippet": null}` and no way to identify the source at all.
|
|
232
|
+
const cd = c?.citationData ?? c;
|
|
233
|
+
const md = cd?.metadata ?? c?.metadata ?? {};
|
|
234
|
+
const content = typeof cd?.content === "string" ? cd.content : c?.content;
|
|
228
235
|
return {
|
|
229
|
-
recordId: md.recordId ?? c?.recordId,
|
|
230
|
-
recordName: md.recordName ?? c?.recordName,
|
|
231
|
-
snippet: typeof
|
|
232
|
-
?
|
|
236
|
+
recordId: md.recordId ?? cd?.recordId ?? c?.recordId,
|
|
237
|
+
recordName: md.recordName ?? cd?.recordName ?? c?.recordName,
|
|
238
|
+
snippet: typeof content === "string"
|
|
239
|
+
? content.slice(0, 280)
|
|
233
240
|
: md.blockText ?? null,
|
|
234
241
|
mimeType: md.mimeType,
|
|
235
242
|
webUrl: md.webUrl,
|
|
236
243
|
pageNum: md.pageNum,
|
|
237
|
-
chunkIndex: c?.chunkIndex ?? md.chunkIndex,
|
|
244
|
+
chunkIndex: cd?.chunkIndex ?? c?.chunkIndex ?? md.chunkIndex,
|
|
238
245
|
};
|
|
239
246
|
});
|
|
240
247
|
}
|