@pipeshub-ai/mcp 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/mcp-server.js +703 -189
- package/bin/mcp-server.js.map +19 -13
- 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 +235 -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 +20 -3
- package/esm/mcp-server/tools/_helpers.d.ts.map +1 -1
- package/esm/mcp-server/tools/_helpers.js +54 -10
- 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/pipeshubDirectory.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubDirectory.js +62 -8
- package/esm/mcp-server/tools/pipeshubDirectory.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 +15 -2
- package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubSources.js +2 -2
- package/esm/mcp-server/tools/pipeshubSources.js.map +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/tool-names.js +4 -4
- 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 +279 -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 +55 -8
- package/src/mcp-server/tools/pipeshubChat.ts +78 -97
- package/src/mcp-server/tools/pipeshubDirectory.ts +65 -7
- package/src/mcp-server/tools/pipeshubGetRecordContent.ts +213 -29
- package/src/mcp-server/tools/pipeshubSearch.ts +15 -2
- package/src/mcp-server/tools/pipeshubSources.ts +2 -2
- package/src/models/lookuprecordbyidentifierop.ts +26 -0
- package/src/models/navigateknowledgegraphop.ts +56 -0
- package/src/tool-names.ts +4 -4
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from Speakeasy-generated connectorLookupRecordByIdentifier.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PipeshubCore } from "../core.js";
|
|
6
|
+
import { encodeFormQuery } from "../lib/encodings.js";
|
|
7
|
+
import { compactMap } from "../lib/primitives.js";
|
|
8
|
+
import { safeParse } from "../lib/schemas.js";
|
|
9
|
+
import { RequestOptions } from "../lib/sdks.js";
|
|
10
|
+
import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js";
|
|
11
|
+
import { pathToFunc } from "../lib/url.js";
|
|
12
|
+
import { APIError } from "../models/errors/apierror.js";
|
|
13
|
+
import {
|
|
14
|
+
ConnectionError,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
RequestAbortedError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
UnexpectedClientError,
|
|
19
|
+
} from "../models/errors/httpclienterrors.js";
|
|
20
|
+
import { SDKValidationError } from "../models/errors/sdkvalidationerror.js";
|
|
21
|
+
import {
|
|
22
|
+
LookupRecordByIdentifierRequest,
|
|
23
|
+
LookupRecordByIdentifierRequest$zodSchema,
|
|
24
|
+
} from "../models/lookuprecordbyidentifierop.js";
|
|
25
|
+
import { APICall, APIPromise } from "../types/async.js";
|
|
26
|
+
import { Result } from "../types/fp.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a URL, issue key or external ID to a Record ID
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Turn an external reference into the matching PipesHub record. Accepts a
|
|
33
|
+
* pasted link, a Jira-style issue key, or a bare external system ID from
|
|
34
|
+
* any connected source. Repeat `identifiers` to batch-resolve up to ten
|
|
35
|
+
* in one call.
|
|
36
|
+
*
|
|
37
|
+
* The response carries a rendered `text` view — each match's metadata
|
|
38
|
+
* block followed by a `Next:` line naming a follow-up call. The structured
|
|
39
|
+
* fields carry the same information for programmatic use.
|
|
40
|
+
*
|
|
41
|
+
* **Accepted identifiers**
|
|
42
|
+
* - Jira issue URL — `https://acme.atlassian.net/browse/PA-1787`
|
|
43
|
+
* - Jira issue key — `PA-1787`
|
|
44
|
+
* - Confluence page URL —
|
|
45
|
+
* `https://acme.atlassian.net/wiki/spaces/SD/pages/450625553/Agent+Loop`
|
|
46
|
+
* - Google Drive / Docs URL — `https://docs.google.com/document/d/1AbC.../edit`
|
|
47
|
+
* - Slack message link — `https://acme.slack.com/archives/C0123/p1720000000000100`
|
|
48
|
+
* - Bare external system ID — `450625553`
|
|
49
|
+
*
|
|
50
|
+
* **When to use this vs. the other record endpoints:**
|
|
51
|
+
* - **This endpoint** converts an *external* reference into an internal
|
|
52
|
+
* Record ID. Reach for it whenever you meet a link or ticket key and
|
|
53
|
+
* need the record behind it.
|
|
54
|
+
* - `GET /connectors/record/{recordId}/content` reads a record you have
|
|
55
|
+
* already identified. It needs an internal Record ID, which is exactly
|
|
56
|
+
* what this endpoint returns.
|
|
57
|
+
* - `GET /connectors/navigate` browses the hierarchy when you have a
|
|
58
|
+
* position in the tree rather than a specific identifier.
|
|
59
|
+
*
|
|
60
|
+
* **Typical flow:** call this with the reference, take a match's `id`
|
|
61
|
+
* from the response, then call
|
|
62
|
+
* `GET /connectors/record/{recordId}/content` to read the record.
|
|
63
|
+
*
|
|
64
|
+
* **Multiple matches:** one identifier can legitimately match more than
|
|
65
|
+
* one record — the same external ID may exist in several connected
|
|
66
|
+
* instances. In that case the response sets `ambiguous: true` and
|
|
67
|
+
* `matches` holds every candidate. Present the choice rather than taking
|
|
68
|
+
* the first; `connectorName` narrows a retry.
|
|
69
|
+
*
|
|
70
|
+
* **Misses are not errors.**
|
|
71
|
+
*
|
|
72
|
+
* Only records the caller can see are returned, and a miss is a `200`
|
|
73
|
+
* with an empty `matches` array and the input echoed in
|
|
74
|
+
* `not_found_identifiers` — not a `404`. The identifier resolved to
|
|
75
|
+
* nothing *or* to something the caller may not access; the two are
|
|
76
|
+
* deliberately indistinguishable, because an identifier is
|
|
77
|
+
* caller-supplied and guessable, and confirming existence would leak
|
|
78
|
+
* records across organizations. `searched_connectors` names what was
|
|
79
|
+
* covered, so a retry with `connectorName` is often the right next move.
|
|
80
|
+
*
|
|
81
|
+
* **Scope:** resolution searches every connector the caller can access,
|
|
82
|
+
* regardless of any source filter used elsewhere.
|
|
83
|
+
*
|
|
84
|
+
* If set, this operation will use either {@link Security.bearerAuth} or {@link Security.oauth2} from the global security.
|
|
85
|
+
*/
|
|
86
|
+
export function connectorLookupRecordByIdentifier(
|
|
87
|
+
client$: PipeshubCore,
|
|
88
|
+
request: LookupRecordByIdentifierRequest,
|
|
89
|
+
options?: RequestOptions,
|
|
90
|
+
): APIPromise<
|
|
91
|
+
Result<
|
|
92
|
+
Response,
|
|
93
|
+
| APIError
|
|
94
|
+
| SDKValidationError
|
|
95
|
+
| UnexpectedClientError
|
|
96
|
+
| InvalidRequestError
|
|
97
|
+
| RequestAbortedError
|
|
98
|
+
| RequestTimeoutError
|
|
99
|
+
| ConnectionError
|
|
100
|
+
>
|
|
101
|
+
> {
|
|
102
|
+
return new APIPromise($do(
|
|
103
|
+
client$,
|
|
104
|
+
request,
|
|
105
|
+
options,
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function $do(
|
|
110
|
+
client$: PipeshubCore,
|
|
111
|
+
request: LookupRecordByIdentifierRequest,
|
|
112
|
+
options?: RequestOptions,
|
|
113
|
+
): Promise<
|
|
114
|
+
[
|
|
115
|
+
Result<
|
|
116
|
+
Response,
|
|
117
|
+
| APIError
|
|
118
|
+
| SDKValidationError
|
|
119
|
+
| UnexpectedClientError
|
|
120
|
+
| InvalidRequestError
|
|
121
|
+
| RequestAbortedError
|
|
122
|
+
| RequestTimeoutError
|
|
123
|
+
| ConnectionError
|
|
124
|
+
>,
|
|
125
|
+
APICall,
|
|
126
|
+
]
|
|
127
|
+
> {
|
|
128
|
+
const parsed$ = safeParse(
|
|
129
|
+
request,
|
|
130
|
+
(value$) => LookupRecordByIdentifierRequest$zodSchema.parse(value$),
|
|
131
|
+
"Input validation failed",
|
|
132
|
+
);
|
|
133
|
+
if (!parsed$.ok) {
|
|
134
|
+
return [parsed$, { status: "invalid" }];
|
|
135
|
+
}
|
|
136
|
+
const payload$ = parsed$.value;
|
|
137
|
+
const body$ = null;
|
|
138
|
+
const path$ = pathToFunc("/connectors/record/lookup")();
|
|
139
|
+
const query$ = encodeFormQuery({
|
|
140
|
+
"connectorName": payload$.connectorName,
|
|
141
|
+
"identifiers": payload$.identifiers,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const headers$ = new Headers(compactMap({
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
}));
|
|
147
|
+
const securityInput = await extractSecurity(client$._options.security);
|
|
148
|
+
const requestSecurity = resolveGlobalSecurity(securityInput);
|
|
149
|
+
|
|
150
|
+
const context = {
|
|
151
|
+
options: client$._options,
|
|
152
|
+
baseURL: options?.serverURL ?? client$._baseURL ?? "",
|
|
153
|
+
operationID: "lookupRecordByIdentifier",
|
|
154
|
+
oAuth2Scopes: ["kb:read", "connector:read"],
|
|
155
|
+
resolvedSecurity: requestSecurity,
|
|
156
|
+
securitySource: client$._options.security,
|
|
157
|
+
retryConfig: options?.retries
|
|
158
|
+
|| client$._options.retryConfig
|
|
159
|
+
|| { strategy: "none" },
|
|
160
|
+
retryCodes: options?.retryCodes || [
|
|
161
|
+
"429",
|
|
162
|
+
"500",
|
|
163
|
+
"502",
|
|
164
|
+
"503",
|
|
165
|
+
"504",
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const requestRes = client$._createRequest(context, {
|
|
170
|
+
security: requestSecurity,
|
|
171
|
+
method: "GET",
|
|
172
|
+
baseURL: options?.serverURL,
|
|
173
|
+
path: path$,
|
|
174
|
+
headers: headers$,
|
|
175
|
+
query: query$,
|
|
176
|
+
body: body$,
|
|
177
|
+
userAgent: client$._options.userAgent,
|
|
178
|
+
timeoutMs: options?.timeoutMs || client$._options.timeoutMs
|
|
179
|
+
|| -1,
|
|
180
|
+
}, options);
|
|
181
|
+
if (!requestRes.ok) {
|
|
182
|
+
return [requestRes, { status: "invalid" }];
|
|
183
|
+
}
|
|
184
|
+
const req$ = requestRes.value;
|
|
185
|
+
|
|
186
|
+
const doResult = await client$._do(req$, {
|
|
187
|
+
context,
|
|
188
|
+
errorCodes: [],
|
|
189
|
+
retryConfig: context.retryConfig,
|
|
190
|
+
retryCodes: context.retryCodes,
|
|
191
|
+
});
|
|
192
|
+
if (!doResult.ok) {
|
|
193
|
+
return [doResult, { status: "request-error", request: req$ }];
|
|
194
|
+
}
|
|
195
|
+
return [doResult, {
|
|
196
|
+
status: "complete",
|
|
197
|
+
"request": req$,
|
|
198
|
+
response: doResult.value,
|
|
199
|
+
}];
|
|
200
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from Speakeasy-generated connectorNavigateKnowledgeGraph.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PipeshubCore } from "../core.js";
|
|
6
|
+
import { encodeFormQuery } from "../lib/encodings.js";
|
|
7
|
+
import { compactMap } from "../lib/primitives.js";
|
|
8
|
+
import { safeParse } from "../lib/schemas.js";
|
|
9
|
+
import { RequestOptions } from "../lib/sdks.js";
|
|
10
|
+
import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js";
|
|
11
|
+
import { pathToFunc } from "../lib/url.js";
|
|
12
|
+
import { APIError } from "../models/errors/apierror.js";
|
|
13
|
+
import {
|
|
14
|
+
ConnectionError,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
RequestAbortedError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
UnexpectedClientError,
|
|
19
|
+
} from "../models/errors/httpclienterrors.js";
|
|
20
|
+
import { SDKValidationError } from "../models/errors/sdkvalidationerror.js";
|
|
21
|
+
import {
|
|
22
|
+
NavigateKnowledgeGraphRequest,
|
|
23
|
+
NavigateKnowledgeGraphRequest$zodSchema,
|
|
24
|
+
} from "../models/navigateknowledgegraphop.js";
|
|
25
|
+
import { APICall, APIPromise } from "../types/async.js";
|
|
26
|
+
import { Result } from "../types/fp.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Browse the knowledge graph from a node
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Open a node in the knowledge graph and see what is inside it — a file
|
|
33
|
+
* explorer across every connected source.
|
|
34
|
+
*
|
|
35
|
+
* Call it with no `nodeId` for a flat listing of every record group and
|
|
36
|
+
* record the caller can reach, newest first. This is a starting point to
|
|
37
|
+
* pick a node from, not a roster of connected apps — app nodes are never
|
|
38
|
+
* returned in a listing, though an app's `id` is accepted as a `nodeId`
|
|
39
|
+
* and lists that app's record groups. Pass a node's `id` to descend:
|
|
40
|
+
* record groups contain records and folders, and a record contains its
|
|
41
|
+
* own children — comments, attachments, sub-tasks — plus a `related`
|
|
42
|
+
* section of cross-referenced records, such as the Confluence page linked
|
|
43
|
+
* from a Jira ticket. `nodeId` is tolerant: a URL or an issue key such as
|
|
44
|
+
* `PA-1787` is resolved to its record before navigating, so a link can be
|
|
45
|
+
* pasted straight in without a separate lookup call.
|
|
46
|
+
*
|
|
47
|
+
* The response carries a rendered `text` view — breadcrumbs, the current
|
|
48
|
+
* node, the children listing, `Related:`, and a closing `Next:` line
|
|
49
|
+
* naming a follow-up call. The structured fields carry the same
|
|
50
|
+
* information for programmatic use.
|
|
51
|
+
*
|
|
52
|
+
* **When to use this vs. the other record endpoints:**
|
|
53
|
+
* - **This endpoint** is for structural exploration — "what is in this
|
|
54
|
+
* project", "what is attached to this ticket", "what else links to this
|
|
55
|
+
* page". It returns names, types and IDs; it never returns document
|
|
56
|
+
* text.
|
|
57
|
+
* - `GET /connectors/record/{recordId}/content` returns one record's
|
|
58
|
+
* actual parsed text. Use it once navigation has identified the record
|
|
59
|
+
* you want to read.
|
|
60
|
+
* - `GET /connectors/record/lookup` is the way in when you hold a URL or
|
|
61
|
+
* an issue key rather than a position in the tree.
|
|
62
|
+
*
|
|
63
|
+
* **Typical flow:** call with no `nodeId` to see what is reachable → pass
|
|
64
|
+
* a record group's `id` to list its records → take a row whose
|
|
65
|
+
* `is_record` is true and call
|
|
66
|
+
* `GET /connectors/record/{recordId}/content` to read it.
|
|
67
|
+
*
|
|
68
|
+
* **Paging and depth:** results are paginated; `pagination.has_next`
|
|
69
|
+
* tells you whether to request the next `page`. `depth` above 1 returns
|
|
70
|
+
* all descendants down to that level as one flat list, each row carrying
|
|
71
|
+
* its own `level`, instead of only direct children.
|
|
72
|
+
*
|
|
73
|
+
* **Scope:** everything the caller can read, across both connectors and
|
|
74
|
+
* Knowledge Base collections. No connector-level filter is applied — the
|
|
75
|
+
* listing is bounded by per-node permissions alone.
|
|
76
|
+
*
|
|
77
|
+
* **Permission scoping:**
|
|
78
|
+
*
|
|
79
|
+
* `rows` and `related` carry only nodes the caller can see, and the
|
|
80
|
+
* opened node itself is access-checked before any of its details are
|
|
81
|
+
* returned. A node that does not exist and a node the caller cannot
|
|
82
|
+
* access are deliberately indistinguishable — both return an empty view
|
|
83
|
+
* rather than an error.
|
|
84
|
+
*
|
|
85
|
+
* `breadcrumbs` is the exception: the ancestor trail is resolved by id
|
|
86
|
+
* alone, without a permission check. For a record shared directly with
|
|
87
|
+
* the caller, it can therefore name ancestors the caller cannot open.
|
|
88
|
+
* Treat breadcrumb entries as labels, not as nodes guaranteed to be
|
|
89
|
+
* navigable.
|
|
90
|
+
*
|
|
91
|
+
* If set, this operation will use either {@link Security.bearerAuth} or {@link Security.oauth2} from the global security.
|
|
92
|
+
*/
|
|
93
|
+
export function connectorNavigateKnowledgeGraph(
|
|
94
|
+
client$: PipeshubCore,
|
|
95
|
+
request?: NavigateKnowledgeGraphRequest | undefined,
|
|
96
|
+
options?: RequestOptions,
|
|
97
|
+
): APIPromise<
|
|
98
|
+
Result<
|
|
99
|
+
Response,
|
|
100
|
+
| APIError
|
|
101
|
+
| SDKValidationError
|
|
102
|
+
| UnexpectedClientError
|
|
103
|
+
| InvalidRequestError
|
|
104
|
+
| RequestAbortedError
|
|
105
|
+
| RequestTimeoutError
|
|
106
|
+
| ConnectionError
|
|
107
|
+
>
|
|
108
|
+
> {
|
|
109
|
+
return new APIPromise($do(
|
|
110
|
+
client$,
|
|
111
|
+
request,
|
|
112
|
+
options,
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function $do(
|
|
117
|
+
client$: PipeshubCore,
|
|
118
|
+
request?: NavigateKnowledgeGraphRequest | undefined,
|
|
119
|
+
options?: RequestOptions,
|
|
120
|
+
): Promise<
|
|
121
|
+
[
|
|
122
|
+
Result<
|
|
123
|
+
Response,
|
|
124
|
+
| APIError
|
|
125
|
+
| SDKValidationError
|
|
126
|
+
| UnexpectedClientError
|
|
127
|
+
| InvalidRequestError
|
|
128
|
+
| RequestAbortedError
|
|
129
|
+
| RequestTimeoutError
|
|
130
|
+
| ConnectionError
|
|
131
|
+
>,
|
|
132
|
+
APICall,
|
|
133
|
+
]
|
|
134
|
+
> {
|
|
135
|
+
const parsed$ = safeParse(
|
|
136
|
+
request,
|
|
137
|
+
(value$) =>
|
|
138
|
+
NavigateKnowledgeGraphRequest$zodSchema.optional().parse(value$),
|
|
139
|
+
"Input validation failed",
|
|
140
|
+
);
|
|
141
|
+
if (!parsed$.ok) {
|
|
142
|
+
return [parsed$, { status: "invalid" }];
|
|
143
|
+
}
|
|
144
|
+
const payload$ = parsed$.value;
|
|
145
|
+
const body$ = null;
|
|
146
|
+
const path$ = pathToFunc("/connectors/navigate")();
|
|
147
|
+
const query$ = encodeFormQuery({
|
|
148
|
+
"createdAfter": payload$?.createdAfter,
|
|
149
|
+
"createdBefore": payload$?.createdBefore,
|
|
150
|
+
"depth": payload$?.depth,
|
|
151
|
+
"limit": payload$?.limit,
|
|
152
|
+
"modifiedAfter": payload$?.modifiedAfter,
|
|
153
|
+
"modifiedBefore": payload$?.modifiedBefore,
|
|
154
|
+
"nodeId": payload$?.nodeId,
|
|
155
|
+
"nodeTypes": payload$?.nodeTypes,
|
|
156
|
+
"page": payload$?.page,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const headers$ = new Headers(compactMap({
|
|
160
|
+
Accept: "application/json",
|
|
161
|
+
}));
|
|
162
|
+
const securityInput = await extractSecurity(client$._options.security);
|
|
163
|
+
const requestSecurity = resolveGlobalSecurity(securityInput);
|
|
164
|
+
|
|
165
|
+
const context = {
|
|
166
|
+
options: client$._options,
|
|
167
|
+
baseURL: options?.serverURL ?? client$._baseURL ?? "",
|
|
168
|
+
operationID: "navigateKnowledgeGraph",
|
|
169
|
+
oAuth2Scopes: ["kb:read", "connector:read"],
|
|
170
|
+
resolvedSecurity: requestSecurity,
|
|
171
|
+
securitySource: client$._options.security,
|
|
172
|
+
retryConfig: options?.retries
|
|
173
|
+
|| client$._options.retryConfig
|
|
174
|
+
|| { strategy: "none" },
|
|
175
|
+
retryCodes: options?.retryCodes || [
|
|
176
|
+
"429",
|
|
177
|
+
"500",
|
|
178
|
+
"502",
|
|
179
|
+
"503",
|
|
180
|
+
"504",
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const requestRes = client$._createRequest(context, {
|
|
185
|
+
security: requestSecurity,
|
|
186
|
+
method: "GET",
|
|
187
|
+
baseURL: options?.serverURL,
|
|
188
|
+
path: path$,
|
|
189
|
+
headers: headers$,
|
|
190
|
+
query: query$,
|
|
191
|
+
body: body$,
|
|
192
|
+
userAgent: client$._options.userAgent,
|
|
193
|
+
timeoutMs: options?.timeoutMs || client$._options.timeoutMs
|
|
194
|
+
|| -1,
|
|
195
|
+
}, options);
|
|
196
|
+
if (!requestRes.ok) {
|
|
197
|
+
return [requestRes, { status: "invalid" }];
|
|
198
|
+
}
|
|
199
|
+
const req$ = requestRes.value;
|
|
200
|
+
|
|
201
|
+
const doResult = await client$._do(req$, {
|
|
202
|
+
context,
|
|
203
|
+
errorCodes: [],
|
|
204
|
+
retryConfig: context.retryConfig,
|
|
205
|
+
retryCodes: context.retryCodes,
|
|
206
|
+
});
|
|
207
|
+
if (!doResult.ok) {
|
|
208
|
+
return [doResult, { status: "request-error", request: req$ }];
|
|
209
|
+
}
|
|
210
|
+
return [doResult, {
|
|
211
|
+
status: "complete",
|
|
212
|
+
"request": req$,
|
|
213
|
+
response: doResult.value,
|
|
214
|
+
}];
|
|
215
|
+
}
|
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
|
+
}
|
|
@@ -71,6 +71,36 @@ the user can verify.
|
|
|
71
71
|
- \`pipeshub_agents\` — list the org's configured **agents** (specialized
|
|
72
72
|
assistants with their own prompt, tools, and knowledge scope).
|
|
73
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
|
+
|
|
74
104
|
## Agents
|
|
75
105
|
|
|
76
106
|
Some orgs configure **agents** for specific jobs (e.g. a Slack messenger, a
|
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
|
+
}
|