@pipeshub-ai/mcp 2.0.1 → 2.2.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 +90 -2
- package/bin/mcp-server.js +1156 -213
- package/bin/mcp-server.js.map +30 -16
- package/esm/funcs/agentsListAgents.d.ts +10 -0
- package/esm/funcs/agentsListAgents.d.ts.map +1 -0
- package/esm/funcs/agentsListAgents.js +83 -0
- package/esm/funcs/agentsListAgents.js.map +1 -0
- package/esm/funcs/agentsStreamConversation.d.ts +10 -0
- package/esm/funcs/agentsStreamConversation.d.ts.map +1 -0
- package/esm/funcs/agentsStreamConversation.js +83 -0
- package/esm/funcs/agentsStreamConversation.js.map +1 -0
- package/esm/funcs/agentsStreamMessage.d.ts +10 -0
- package/esm/funcs/agentsStreamMessage.d.ts.map +1 -0
- package/esm/funcs/agentsStreamMessage.js +86 -0
- package/esm/funcs/agentsStreamMessage.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/conversationsStreamConversation.d.ts.map +1 -1
- package/esm/funcs/conversationsStreamConversation.js +2 -4
- package/esm/funcs/conversationsStreamConversation.js.map +1 -1
- package/esm/funcs/conversationsStreamMessage.d.ts.map +1 -1
- package/esm/funcs/conversationsStreamMessage.js +0 -7
- package/esm/funcs/conversationsStreamMessage.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/mcp-server/instructions.d.ts +2 -0
- package/esm/mcp-server/instructions.d.ts.map +1 -0
- package/esm/mcp-server/instructions.js +106 -0
- package/esm/mcp-server/instructions.js.map +1 -0
- package/esm/mcp-server/prompts/pipeshubAssistant.d.ts +8 -0
- package/esm/mcp-server/prompts/pipeshubAssistant.d.ts.map +1 -0
- package/esm/mcp-server/prompts/pipeshubAssistant.js +31 -0
- package/esm/mcp-server/prompts/pipeshubAssistant.js.map +1 -0
- package/esm/mcp-server/server.d.ts.map +1 -1
- package/esm/mcp-server/server.js +16 -62
- package/esm/mcp-server/server.js.map +1 -1
- package/esm/mcp-server/tools/_helpers.d.ts +78 -0
- package/esm/mcp-server/tools/_helpers.d.ts.map +1 -1
- package/esm/mcp-server/tools/_helpers.js +155 -1
- package/esm/mcp-server/tools/_helpers.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubAgents.d.ts +8 -0
- package/esm/mcp-server/tools/pipeshubAgents.d.ts.map +1 -0
- package/esm/mcp-server/tools/pipeshubAgents.js +62 -0
- package/esm/mcp-server/tools/pipeshubAgents.js.map +1 -0
- package/esm/mcp-server/tools/pipeshubChat.d.ts +6 -1
- package/esm/mcp-server/tools/pipeshubChat.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubChat.js +79 -23
- 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 +0 -1
- package/esm/mcp-server/tools/pipeshubDirectory.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubDownloadRecord.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubDownloadRecord.js +0 -3
- package/esm/mcp-server/tools/pipeshubDownloadRecord.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts +8 -0
- package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts.map +1 -0
- package/esm/mcp-server/tools/pipeshubGetRecordContent.js +63 -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 +8 -7
- package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
- package/esm/mcp-server/tools/pipeshubSources.d.ts.map +1 -1
- package/esm/mcp-server/tools/pipeshubSources.js +0 -4
- package/esm/mcp-server/tools/pipeshubSources.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/agentops.d.ts +119 -0
- package/esm/models/agentops.d.ts.map +1 -0
- package/esm/models/agentops.js +97 -0
- package/esm/models/agentops.js.map +1 -0
- 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/tool-names.d.ts.map +1 -1
- package/esm/tool-names.js +10 -2
- package/esm/tool-names.js.map +1 -1
- package/package.json +3 -2
- package/src/funcs/agentsListAgents.ts +141 -0
- package/src/funcs/agentsStreamConversation.ts +144 -0
- package/src/funcs/agentsStreamMessage.ts +147 -0
- package/src/funcs/connectorGetRecordContent.ts +179 -0
- package/src/funcs/conversationsStreamConversation.ts +2 -4
- package/src/funcs/conversationsStreamMessage.ts +0 -8
- 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/mcp-server/instructions.ts +106 -0
- package/src/mcp-server/prompts/pipeshubAssistant.ts +37 -0
- package/src/mcp-server/server.ts +18 -64
- package/src/mcp-server/tools/_helpers.ts +216 -1
- package/src/mcp-server/tools/pipeshubAgents.ts +68 -0
- package/src/mcp-server/tools/pipeshubChat.ts +78 -23
- package/src/mcp-server/tools/pipeshubDirectory.ts +0 -1
- package/src/mcp-server/tools/pipeshubDownloadRecord.ts +0 -4
- package/src/mcp-server/tools/pipeshubGetRecordContent.ts +67 -0
- package/src/mcp-server/tools/pipeshubSearch.ts +8 -7
- package/src/mcp-server/tools/pipeshubSources.ts +0 -5
- package/src/mcp-server/tools.ts +14 -1
- package/src/models/agentops.ts +160 -0
- package/src/models/getrecordcontentop.ts +11 -0
- package/src/tool-names.ts +10 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Ambient per-tool-call request-id context (AsyncLocalStorage). Mirrors the
|
|
3
|
+
* pipeshub-ai backend's own request-context pattern (same header name,
|
|
4
|
+
* `x-request-id`) so a single id traces one MCP tool/resource/prompt
|
|
5
|
+
* invocation across every backend HTTP call it makes.
|
|
6
|
+
*
|
|
7
|
+
* `tools.ts` / `resources.ts` / `prompts.ts` are Speakeasy-generated and
|
|
8
|
+
* regenerated on every build, so there is no `next()`-style wrapper point to
|
|
9
|
+
* bind context around a call. Instead `bindNewRequestId` is called from the
|
|
10
|
+
* hand-written `getClient` closure in `server.ts`, which every generated
|
|
11
|
+
* dispatch site calls synchronously as the first thing before invoking a
|
|
12
|
+
* tool/resource/prompt handler — `enterWith` binds the store for the rest of
|
|
13
|
+
* that handler's execution (sync remainder + everything it awaits).
|
|
14
|
+
*
|
|
15
|
+
* The final id (`mcp-<userId>-<random>` or `mcp-<random>`) can't be composed
|
|
16
|
+
* at bind time — `getClient` only has a `PipeshubCore`, not yet a resolved
|
|
17
|
+
* `Authorization` header, and resolving security may be async (OAuth2)
|
|
18
|
+
* while `getClient` must stay sync (generated call sites pass its return
|
|
19
|
+
* value straight into a handler, never await it). So only the random suffix
|
|
20
|
+
* is fixed here; `requestid.ts`'s hook fills in `finalId` off the first
|
|
21
|
+
* outbound request's `Authorization` header and every later call in the
|
|
22
|
+
* same tool invocation reuses that cached value.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
|
+
import { nanoid } from "nanoid";
|
|
27
|
+
|
|
28
|
+
export interface RequestContextStore {
|
|
29
|
+
random: string;
|
|
30
|
+
finalId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const storage = new AsyncLocalStorage<RequestContextStore>();
|
|
34
|
+
|
|
35
|
+
/** Bind a fresh request-id context for the remainder of the current async chain. */
|
|
36
|
+
export function bindNewRequestId(): void {
|
|
37
|
+
storage.enterWith({ random: nanoid() });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getRequestContextStore(): RequestContextStore | undefined {
|
|
41
|
+
return storage.getStore();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One-off id for the rare case a backend call happens with no bound tool-call context. */
|
|
45
|
+
export function newRequestId(): string {
|
|
46
|
+
return `mcp-${nanoid()}`;
|
|
47
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Shared routing guidance for the host LLM. Surfaced two ways:
|
|
2
|
+
// 1. As the server `instructions` (always present at `initialize`).
|
|
3
|
+
// 2. As the body of the user-invokable `pipeshub-assistant` prompt.
|
|
4
|
+
// Keeping it in one place avoids the two copies drifting apart.
|
|
5
|
+
|
|
6
|
+
export const PIPESHUB_INSTRUCTIONS = `# PipesHub MCP — instructions for the LLM
|
|
7
|
+
|
|
8
|
+
PipesHub is the user's workplace AI platform. It indexes their documents,
|
|
9
|
+
knowledge base content, and connector sources (Drive, Box, Confluence,
|
|
10
|
+
Slack, Jira, Gmail, ...). When in doubt, the answer is in PipesHub.
|
|
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
|
+
|
|
33
|
+
## Default tool: \`pipeshub_chat\`
|
|
34
|
+
|
|
35
|
+
**Use \`pipeshub_chat\` for any question that could plausibly be answered
|
|
36
|
+
by the user's PipesHub-indexed data** and is not a full-document task:
|
|
37
|
+
|
|
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?").
|
|
41
|
+
- Anything about company / org policies, processes, decisions, or
|
|
42
|
+
history (e.g. "what's our vacation policy?", "who owns the auth
|
|
43
|
+
service?").
|
|
44
|
+
- Anything explicitly mentioning PipesHub itself, or its sources
|
|
45
|
+
(Drive / Box / Confluence / Slack / Gmail / Jira / etc.) when the
|
|
46
|
+
user has those connected.
|
|
47
|
+
- Open-ended "what do we know about X" questions where the answer
|
|
48
|
+
likely lives in the org's documents.
|
|
49
|
+
|
|
50
|
+
Do NOT answer those from your own knowledge — \`pipeshub_chat\` grounds
|
|
51
|
+
the answer in the user's actual indexed content and returns citations
|
|
52
|
+
the user can verify.
|
|
53
|
+
|
|
54
|
+
## When to use the other tools
|
|
55
|
+
|
|
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\`.
|
|
59
|
+
- \`pipeshub_download_record\` — when the user wants the actual file
|
|
60
|
+
bytes (download, attach, open). Get the \`recordId\` either from
|
|
61
|
+
citations on a prior \`pipeshub_chat\` response or from
|
|
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.
|
|
67
|
+
- \`pipeshub_directory\` — people, groups, teams, and \`whoami\` lookups.
|
|
68
|
+
Not for documents.
|
|
69
|
+
- \`pipeshub_sources\` — call once at the start of a session to discover
|
|
70
|
+
which connectors / KB / models are available, then cache the result.
|
|
71
|
+
- \`pipeshub_agents\` — list the org's configured **agents** (specialized
|
|
72
|
+
assistants with their own prompt, tools, and knowledge scope).
|
|
73
|
+
|
|
74
|
+
## Agents
|
|
75
|
+
|
|
76
|
+
Some orgs configure **agents** for specific jobs (e.g. a Slack messenger, a
|
|
77
|
+
Jira ticket creator, a Salesforce CRM updater). To run a turn against an
|
|
78
|
+
agent, call \`pipeshub_chat\` with its \`agentId\` (from \`pipeshub_agents\`)
|
|
79
|
+
— optionally with an agent \`chatMode\` (\`auto\` by default, or \`quick\` /
|
|
80
|
+
\`verification\` / \`deep\`). Keep passing the same \`agentId\` plus the
|
|
81
|
+
returned \`conversationId\` on follow-up turns.
|
|
82
|
+
|
|
83
|
+
**When to route to an agent vs plain chat:**
|
|
84
|
+
- The user names a system/action that maps to an agent (e.g. "post to
|
|
85
|
+
Slack", "create a Jira ticket", "update the Salesforce deal") → call
|
|
86
|
+
\`pipeshub_agents\`, pick the matching agent, and use it.
|
|
87
|
+
- Plain question about the org's knowledge or the public web → just use
|
|
88
|
+
\`pipeshub_chat\` (no \`agentId\`) with \`internal_search\` or \`web_search\`.
|
|
89
|
+
|
|
90
|
+
**If you are unsure which agent or route fits the request**, call
|
|
91
|
+
\`pipeshub_agents\` FIRST and read the returned names/descriptions, then
|
|
92
|
+
either pick the best match or present the list to the user and let them
|
|
93
|
+
choose. Do NOT guess an agent blindly, and do NOT refuse — list the agents
|
|
94
|
+
and decide from real data. The list may be empty (no agents configured),
|
|
95
|
+
in which case fall back to plain \`pipeshub_chat\`.
|
|
96
|
+
|
|
97
|
+
## Conversation lifecycle
|
|
98
|
+
|
|
99
|
+
\`pipeshub_chat\` is a single tool that handles both starting and
|
|
100
|
+
continuing conversations (plain or with an \`agentId\`). On the first turn
|
|
101
|
+
omit \`conversationId\`; on every subsequent turn pass back the
|
|
102
|
+
\`conversationId\` returned by the previous call (along with the same
|
|
103
|
+
\`agentId\` if you used one). Server-side context is preserved — do NOT
|
|
104
|
+
replay prior messages. Only start a fresh conversation when the user
|
|
105
|
+
explicitly asks to clear context.
|
|
106
|
+
`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { PromptDefinition } from "../prompts.js";
|
|
3
|
+
import { PIPESHUB_INSTRUCTIONS } from "../instructions.js";
|
|
4
|
+
|
|
5
|
+
const args = {
|
|
6
|
+
query: z.string().optional().describe(
|
|
7
|
+
"Optional. The user's request to route. Provide it to have the guidance "
|
|
8
|
+
+ "applied to a concrete request; omit it to just load the routing "
|
|
9
|
+
+ "guidance as a seed system prompt at the start of a session.",
|
|
10
|
+
),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const prompt$pipeshubAssistant: PromptDefinition<typeof args> = {
|
|
14
|
+
name: "pipeshub-assistant",
|
|
15
|
+
description:
|
|
16
|
+
"Load PipesHub tool-routing guidance so the assistant picks the correct "
|
|
17
|
+
+ "tool (pipeshub_chat / search / agents / directory) and lists agents "
|
|
18
|
+
+ "with pipeshub_agents when unsure which route fits the query.",
|
|
19
|
+
scopes: ["read"],
|
|
20
|
+
args,
|
|
21
|
+
prompt: (_client, args, _extra) => {
|
|
22
|
+
const text = args.query
|
|
23
|
+
? `${PIPESHUB_INSTRUCTIONS}\n\n---\n\nFollow the routing rules above to `
|
|
24
|
+
+ `handle this request, picking the right tool (and listing agents via `
|
|
25
|
+
+ `\`pipeshub_agents\` if unsure which route fits):\n\n${args.query}`
|
|
26
|
+
: PIPESHUB_INSTRUCTIONS;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
messages: [
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
content: { type: "text", text },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
package/src/mcp-server/server.ts
CHANGED
|
@@ -1,66 +1,8 @@
|
|
|
1
|
-
// Hand-written; replaces the Speakeasy-generated server.ts. Registers the
|
|
2
|
-
// five curated PipesHub tools instead of one tool per OpenAPI operation.
|
|
3
|
-
|
|
4
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
// These set defaults so the model picks `pipeshub_chat` as the natural
|
|
8
|
-
// entry point for anything that touches the user's PipesHub data —
|
|
9
|
-
// documents, files, knowledge base content, citations, etc.
|
|
10
|
-
const PIPESHUB_INSTRUCTIONS = `# PipesHub MCP — instructions for the LLM
|
|
11
|
-
|
|
12
|
-
PipesHub is the user's workplace AI platform. It indexes their documents,
|
|
13
|
-
knowledge base content, and connector sources (Drive, Box, Confluence,
|
|
14
|
-
Slack, Jira, Gmail, ...). When in doubt, the answer is in PipesHub.
|
|
15
|
-
|
|
16
|
-
## Default tool: \`pipeshub_chat\`
|
|
17
|
-
|
|
18
|
-
**Use \`pipeshub_chat\` for any question that could plausibly be answered
|
|
19
|
-
by the user's PipesHub-indexed data.** That includes:
|
|
20
|
-
|
|
21
|
-
- Anything about a specific document, file, report, ticket, message, or
|
|
22
|
-
page (e.g. "what's in the Q4 sales report?", "summarize the langchain
|
|
23
|
-
doc", "what did Aashil say about onboarding?").
|
|
24
|
-
- Anything about company / org policies, processes, decisions, or
|
|
25
|
-
history (e.g. "what's our vacation policy?", "who owns the auth
|
|
26
|
-
service?").
|
|
27
|
-
- Anything explicitly mentioning PipesHub itself, or its sources
|
|
28
|
-
(Drive / Box / Confluence / Slack / Gmail / Jira / etc.) when the
|
|
29
|
-
user has those connected.
|
|
30
|
-
- Open-ended "what do we know about X" questions where the answer
|
|
31
|
-
likely lives in the org's documents.
|
|
32
|
-
|
|
33
|
-
Do NOT answer those from your own knowledge — \`pipeshub_chat\` grounds
|
|
34
|
-
the answer in the user's actual indexed content and returns citations
|
|
35
|
-
the user can verify.
|
|
36
|
-
|
|
37
|
-
## When to use the other tools
|
|
38
|
-
|
|
39
|
-
- \`pipeshub_search\` — only when the user asks specifically to **find /
|
|
40
|
-
locate** a document by name or topic (so you can hand them a list, or
|
|
41
|
-
resolve a filename to a \`recordId\` for download). For "what does
|
|
42
|
-
the doc say?" use \`pipeshub_chat\` instead — it does the retrieval
|
|
43
|
-
internally.
|
|
44
|
-
- \`pipeshub_download_record\` — when the user wants the actual file
|
|
45
|
-
bytes (download, attach, open). Get the \`recordId\` either from
|
|
46
|
-
citations on a prior \`pipeshub_chat\` response or from
|
|
47
|
-
\`pipeshub_search\`.
|
|
48
|
-
- \`pipeshub_directory\` — people, groups, teams, and \`whoami\` lookups.
|
|
49
|
-
Not for documents.
|
|
50
|
-
- \`pipeshub_sources\` — call once at the start of a session to discover
|
|
51
|
-
which connectors / KB / models are available, then cache the result.
|
|
52
|
-
|
|
53
|
-
## Conversation lifecycle
|
|
54
|
-
|
|
55
|
-
\`pipeshub_chat\` is a single tool that handles both starting and
|
|
56
|
-
continuing conversations. On the first turn omit \`conversationId\`; on
|
|
57
|
-
every subsequent turn pass back the \`conversationId\` returned by the
|
|
58
|
-
previous call. Server-side context is preserved — do NOT replay prior
|
|
59
|
-
messages. Only start a fresh conversation when the user explicitly
|
|
60
|
-
asks to clear context.
|
|
61
|
-
`;
|
|
62
|
-
|
|
3
|
+
import { PIPESHUB_INSTRUCTIONS } from "./instructions.js";
|
|
63
4
|
import { PipeshubCore } from "../core.js";
|
|
5
|
+
import { bindNewRequestId } from "../hooks/request-context.js";
|
|
64
6
|
import { SDKOptions } from "../lib/config.js";
|
|
65
7
|
import type { ConsoleLogger } from "./console-logger.js";
|
|
66
8
|
import { createRegisterPrompt } from "./prompts.js";
|
|
@@ -73,8 +15,11 @@ import { createRegisterTool, registerDynamicTools } from "./tools.js";
|
|
|
73
15
|
import { tool$pipeshubChat } from "./tools/pipeshubChat.js";
|
|
74
16
|
import { tool$pipeshubSearch } from "./tools/pipeshubSearch.js";
|
|
75
17
|
import { tool$pipeshubDownloadRecord } from "./tools/pipeshubDownloadRecord.js";
|
|
18
|
+
import { tool$pipeshubGetRecordContent } from "./tools/pipeshubGetRecordContent.js";
|
|
76
19
|
import { tool$pipeshubDirectory } from "./tools/pipeshubDirectory.js";
|
|
77
20
|
import { tool$pipeshubSources } from "./tools/pipeshubSources.js";
|
|
21
|
+
import { tool$pipeshubAgents } from "./tools/pipeshubAgents.js";
|
|
22
|
+
import { prompt$pipeshubAssistant } from "./prompts/pipeshubAssistant.js";
|
|
78
23
|
|
|
79
24
|
export function createMCPServer(deps: {
|
|
80
25
|
logger: ConsoleLogger;
|
|
@@ -97,7 +42,7 @@ export function createMCPServer(deps: {
|
|
|
97
42
|
},
|
|
98
43
|
);
|
|
99
44
|
|
|
100
|
-
const
|
|
45
|
+
const resolveClient = deps.getSDK || (() =>
|
|
101
46
|
new PipeshubCore({
|
|
102
47
|
security: deps.security,
|
|
103
48
|
serverURL: deps.serverURL,
|
|
@@ -112,6 +57,11 @@ export function createMCPServer(deps: {
|
|
|
112
57
|
: undefined,
|
|
113
58
|
}));
|
|
114
59
|
|
|
60
|
+
const getClient = () => {
|
|
61
|
+
bindNewRequestId();
|
|
62
|
+
return resolveClient();
|
|
63
|
+
};
|
|
64
|
+
|
|
115
65
|
const scopes = new Set(deps.scopes);
|
|
116
66
|
|
|
117
67
|
const allowedTools = deps.allowedTools && new Set(deps.allowedTools);
|
|
@@ -139,13 +89,17 @@ export function createMCPServer(deps: {
|
|
|
139
89
|
const register = { tool, resource, resourceTemplate, prompt };
|
|
140
90
|
void register; // suppress unused warnings
|
|
141
91
|
|
|
142
|
-
// Curated tool set.
|
|
143
|
-
// Order = preferred discovery order shown to the LLM.
|
|
92
|
+
// Curated tool set. Order = preferred discovery order shown to the LLM.
|
|
144
93
|
tool(tool$pipeshubSources); // 1. discover sources + models
|
|
145
94
|
tool(tool$pipeshubChat); // 2. ask questions (start + continue)
|
|
146
95
|
tool(tool$pipeshubSearch); // 3. resolve filename → recordId
|
|
147
96
|
tool(tool$pipeshubDownloadRecord); // 4. fetch a document by id
|
|
148
|
-
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
|
|
100
|
+
|
|
101
|
+
// Curated prompt: user-invokable tool-routing guidance.
|
|
102
|
+
prompt(prompt$pipeshubAssistant);
|
|
149
103
|
|
|
150
104
|
if (deps.dynamic) {
|
|
151
105
|
registerDynamicTools(deps.logger, server, getClient, toolMap, scopes);
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
// Curated-tool helpers. Hand-written, NOT regenerated by Speakeasy.
|
|
2
1
|
//
|
|
3
2
|
// Each curated tool fetches a Response from one of the SDK funcs and then
|
|
4
3
|
// trims / restructures the body before handing it to the LLM. The helpers
|
|
5
4
|
// here keep that work out of the individual tool files.
|
|
6
5
|
|
|
6
|
+
import * as z from "zod";
|
|
7
7
|
import { Security } from "../../models/security.js";
|
|
8
8
|
import { PipeshubCore } from "../../core.js";
|
|
9
9
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { agentsListAgents } from "../../funcs/agentsListAgents.js";
|
|
11
|
+
import {
|
|
12
|
+
AgentListEnvelope$zodSchema,
|
|
13
|
+
AgentSummary,
|
|
14
|
+
} from "../../models/agentops.js";
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Parse a fetch Response as JSON, returning a CallToolResult error when the
|
|
@@ -58,6 +63,94 @@ export function errorResult(message: string): CallToolResult {
|
|
|
58
63
|
};
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Guard a (possibly streaming) `Response` on its HTTP status. The streaming
|
|
68
|
+
* SDK funcs configure `errorCodes: []`, so the request layer treats ANY HTTP
|
|
69
|
+
* response — including 401 / 403 / 404 / 5xx — as a success and hands back the
|
|
70
|
+
* raw `Response`. Without this check those failures slip into the SSE drain
|
|
71
|
+
* and surface as the useless "stream ended without usable frames".
|
|
72
|
+
*
|
|
73
|
+
* Returns a `CallToolResult` error (status + best-effort message from the body)
|
|
74
|
+
* when the response is not ok, or `null` when it is ok (body left untouched so
|
|
75
|
+
* the caller can still stream it).
|
|
76
|
+
*/
|
|
77
|
+
export async function httpErrorResult(
|
|
78
|
+
response: Response,
|
|
79
|
+
context: string,
|
|
80
|
+
): Promise<CallToolResult | null> {
|
|
81
|
+
if (response.ok) return null;
|
|
82
|
+
|
|
83
|
+
let body = "";
|
|
84
|
+
try {
|
|
85
|
+
body = await response.text();
|
|
86
|
+
} catch {
|
|
87
|
+
// body unreadable (e.g. already-consumed or transport error)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let message: string = body;
|
|
91
|
+
if (body) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(body);
|
|
94
|
+
// Only accept string fields — error envelopes often nest an object
|
|
95
|
+
// under `error`, which must not stringify to "[object Object]".
|
|
96
|
+
const candidate =
|
|
97
|
+
(typeof parsed?.message === "string" && parsed.message)
|
|
98
|
+
|| (typeof parsed?.error === "string" && parsed.error)
|
|
99
|
+
|| (typeof parsed?.error?.message === "string" && parsed.error.message)
|
|
100
|
+
|| (typeof parsed?.detail === "string" && parsed.detail)
|
|
101
|
+
|| "";
|
|
102
|
+
message = candidate || body; // fall back to the raw JSON text
|
|
103
|
+
} catch {
|
|
104
|
+
// not JSON — keep the raw text
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const detail = message ? ` ${message.slice(0, 400)}` : "";
|
|
109
|
+
const auth = (response.status === 401 || response.status === 403)
|
|
110
|
+
? " Check that the bearer token / credentials are valid and not expired."
|
|
111
|
+
: "";
|
|
112
|
+
return errorResult(
|
|
113
|
+
`${context} failed (HTTP ${response.status} ${response.statusText}).${detail}${auth}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read a `Response` body as JSON and validate it against a zod `schema`,
|
|
119
|
+
* returning the typed value or a `CallToolResult` error (empty body, invalid
|
|
120
|
+
* JSON, or a shape that doesn't match the schema).
|
|
121
|
+
*/
|
|
122
|
+
export async function readValidated<T>(
|
|
123
|
+
response: Response,
|
|
124
|
+
schema: z.ZodType<T>,
|
|
125
|
+
): Promise<{ ok: true; value: T } | { ok: false; result: CallToolResult }> {
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
if (!text) {
|
|
128
|
+
return { ok: false, result: errorResult("Empty response from server") };
|
|
129
|
+
}
|
|
130
|
+
let json;
|
|
131
|
+
try {
|
|
132
|
+
json = JSON.parse(text);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
result: errorResult(
|
|
137
|
+
`Failed to parse response as JSON: ${(e as Error).message}`
|
|
138
|
+
+ `\n\nRaw body:\n${text.slice(0, 500)}`,
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const parsed = schema.safeParse(json);
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
result: errorResult(
|
|
147
|
+
`Unexpected response shape:\n${z.prettifyError(parsed.error)}`,
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, value: parsed.data };
|
|
152
|
+
}
|
|
153
|
+
|
|
61
154
|
/** Resolve `client._options.security` (which may be a function) to a value. */
|
|
62
155
|
export async function resolveSecurity(
|
|
63
156
|
client: PipeshubCore,
|
|
@@ -257,6 +350,128 @@ export async function* iterateSSE(
|
|
|
257
350
|
}
|
|
258
351
|
}
|
|
259
352
|
|
|
353
|
+
// ── Trimmed agent shape handed to the LLM ────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
interface AgentToolsetView {
|
|
356
|
+
/** Integration / connector key, e.g. `jira`, `gmail`. */
|
|
357
|
+
name: string | null | undefined;
|
|
358
|
+
/** Fully-qualified tool ids the agent can run, e.g. `jira.create_issue`. */
|
|
359
|
+
tools: string[];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
interface AgentKnowledgeView {
|
|
363
|
+
name: string | null | undefined;
|
|
364
|
+
type: string | null | undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export interface EnrichedAgent {
|
|
368
|
+
agentId: string;
|
|
369
|
+
name: string;
|
|
370
|
+
description: string | null;
|
|
371
|
+
systemPrompt: string | null;
|
|
372
|
+
startMessage: string | null;
|
|
373
|
+
tags: string[];
|
|
374
|
+
webSearch: boolean;
|
|
375
|
+
isActive: boolean | null | undefined;
|
|
376
|
+
toolsets: AgentToolsetView[];
|
|
377
|
+
knowledge: AgentKnowledgeView[];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Trim an enriched `AgentSummary` (from `GET /agents`) to just what the LLM
|
|
382
|
+
* needs to pick and converse with an agent. Strips ids/revs/timestamps/perms
|
|
383
|
+
* and the heavy nested tool/knowledge metadata, keeping the routing-critical
|
|
384
|
+
* signal: each toolset's connector `name` + its tool ids (e.g.
|
|
385
|
+
* `jira.create_issue`), and each knowledge source's `name`/`type`.
|
|
386
|
+
*/
|
|
387
|
+
export function trimAgent(a: AgentSummary): EnrichedAgent {
|
|
388
|
+
return {
|
|
389
|
+
agentId: a._key,
|
|
390
|
+
name: a.name,
|
|
391
|
+
description: a.description ?? null,
|
|
392
|
+
systemPrompt: a.systemPrompt ?? null,
|
|
393
|
+
startMessage: a.startMessage ?? null,
|
|
394
|
+
tags: a.tags,
|
|
395
|
+
webSearch: !!a.webSearch,
|
|
396
|
+
isActive: a.isActive,
|
|
397
|
+
toolsets: a.toolsets.map((ts) => ({
|
|
398
|
+
name: ts.name,
|
|
399
|
+
tools: ts.tools
|
|
400
|
+
.map((t) => t.fullName ?? t.name)
|
|
401
|
+
.filter((id): id is string => typeof id === "string"),
|
|
402
|
+
})),
|
|
403
|
+
knowledge: a.knowledge.map((k) => ({
|
|
404
|
+
name: k.name ?? k.displayName,
|
|
405
|
+
type: k.type,
|
|
406
|
+
})),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fetch the COMPLETE agent list, auto-paginating `GET /agents` so callers
|
|
412
|
+
* (discovery + routing) never have to manage page numbers. Pages are fetched
|
|
413
|
+
* at the max page size until the backend reports no more (`hasNext` /
|
|
414
|
+
* `hasNextPage`, with a short-page fallback), capped by `maxPages` as a
|
|
415
|
+
* runaway guard. Returns the raw `AgentListItem`s plus a normalized `total`
|
|
416
|
+
* and a `truncated` flag (true if the cap was hit before exhausting pages).
|
|
417
|
+
*/
|
|
418
|
+
export async function listAllAgents(
|
|
419
|
+
client: PipeshubCore,
|
|
420
|
+
opts: {
|
|
421
|
+
search?: string | undefined;
|
|
422
|
+
signal?: AbortSignal | undefined;
|
|
423
|
+
maxPages?: number | undefined;
|
|
424
|
+
} = {},
|
|
425
|
+
): Promise<
|
|
426
|
+
| { ok: true; agents: AgentSummary[]; total: number; truncated: boolean }
|
|
427
|
+
| { ok: false; result: CallToolResult }
|
|
428
|
+
> {
|
|
429
|
+
const PAGE_SIZE = 200;
|
|
430
|
+
const maxPages = opts.maxPages ?? 25;
|
|
431
|
+
// Only set `signal` when present (exactOptionalPropertyTypes).
|
|
432
|
+
const reqOptions = opts.signal
|
|
433
|
+
? { fetchOptions: { signal: opts.signal } }
|
|
434
|
+
: {};
|
|
435
|
+
|
|
436
|
+
const all: AgentSummary[] = [];
|
|
437
|
+
let total = 0;
|
|
438
|
+
|
|
439
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
440
|
+
const [r] = await agentsListAgents(client, {
|
|
441
|
+
page,
|
|
442
|
+
limit: PAGE_SIZE,
|
|
443
|
+
search: opts.search,
|
|
444
|
+
}, reqOptions).$inspect();
|
|
445
|
+
if (!r.ok) return { ok: false, result: errorResult(r.error.message) };
|
|
446
|
+
|
|
447
|
+
const httpErr = await httpErrorResult(r.value, "Agent list request");
|
|
448
|
+
if (httpErr) return { ok: false, result: httpErr };
|
|
449
|
+
|
|
450
|
+
const parsed = await readValidated(r.value, AgentListEnvelope$zodSchema);
|
|
451
|
+
if (!parsed.ok) return { ok: false, result: parsed.result };
|
|
452
|
+
if (parsed.value.success === false) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
result: errorResult("Agent list request returned success: false"),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const agents = parsed.value.agents;
|
|
460
|
+
all.push(...agents);
|
|
461
|
+
|
|
462
|
+
const pg = parsed.value.pagination;
|
|
463
|
+
total = pg?.totalItems ?? pg?.total ?? all.length;
|
|
464
|
+
|
|
465
|
+
const hasNext = pg?.hasNext ?? pg?.hasNextPage ?? (agents.length >= PAGE_SIZE);
|
|
466
|
+
if (!hasNext || agents.length === 0) {
|
|
467
|
+
return { ok: true, agents: all, total, truncated: false };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Hit the page cap with more pages still available.
|
|
472
|
+
return { ok: true, agents: all, total: total || all.length, truncated: true };
|
|
473
|
+
}
|
|
474
|
+
|
|
260
475
|
/** Trim a single search hit to what the LLM actually needs. */
|
|
261
476
|
export function trimSearchHit(hit: any) {
|
|
262
477
|
const md = hit?.metadata ?? {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// (`GET /agents`) — each already carrying its `toolsets` and `knowledge` — so
|
|
2
|
+
// the LLM can route on real capability rather than the often-generic
|
|
3
|
+
// description. Pick an agent's `agentId` and pass it to `pipeshub_chat`'s
|
|
4
|
+
// `agentId` to converse with it. Single API call, no per-agent fan-out.
|
|
5
|
+
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { ToolDefinition } from "../tools.js";
|
|
8
|
+
import { jsonResult, listAllAgents, trimAgent } from "./_helpers.js";
|
|
9
|
+
|
|
10
|
+
const args = {
|
|
11
|
+
search: z.string().optional().describe(
|
|
12
|
+
"Optional case-insensitive substring match across agent name, "
|
|
13
|
+
+ "description, and tags. Omit to return every agent.",
|
|
14
|
+
),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const tool$pipeshubAgents: ToolDefinition<typeof args> = {
|
|
18
|
+
name: "pipeshub_agents",
|
|
19
|
+
description:
|
|
20
|
+
`List the PipesHub **agents** configured for this org, each with its
|
|
21
|
+
capabilities.
|
|
22
|
+
|
|
23
|
+
Agents are specialized assistants (custom system prompt, tools, knowledge
|
|
24
|
+
scope). To converse with one, take its \`agentId\` and pass it to
|
|
25
|
+
\`pipeshub_chat\`'s \`agentId\` argument.
|
|
26
|
+
|
|
27
|
+
Each agent is returned as:
|
|
28
|
+
\`{ agentId, name, description, systemPrompt, startMessage, tags, webSearch,
|
|
29
|
+
isActive, toolsets, knowledge }\`.
|
|
30
|
+
- \`toolsets\` — what the agent can DO: each \`{ name, tools }\` where \`name\`
|
|
31
|
+
is the connector (e.g. \`jira\`, \`gmail\`) and \`tools\` are the runnable
|
|
32
|
+
tool ids (e.g. \`jira.create_issue\`, \`gmail.send_email\`).
|
|
33
|
+
- \`knowledge\` — what the agent can READ: each \`{ name, type }\` (e.g.
|
|
34
|
+
\`Confluence-2\` / \`Confluence\`).
|
|
35
|
+
|
|
36
|
+
**Route on \`toolsets\`/\`knowledge\`, not the name** — names and descriptions
|
|
37
|
+
are often generic or misleading. Match the request to the agent whose tools can
|
|
38
|
+
actually perform it (e.g. "create a Jira ticket" → the agent whose toolset is
|
|
39
|
+
\`jira\` and whose tools include \`jira.create_issue\`). If NO agent has a tool
|
|
40
|
+
for the requested action, say so — don't force an unrelated agent.
|
|
41
|
+
|
|
42
|
+
The list **may be empty** (no agents configured). For plain Q&A when no
|
|
43
|
+
specific agent is needed, use \`pipeshub_chat\` WITHOUT \`agentId\` and pick a
|
|
44
|
+
\`chatMode\`: \`internal_search\` (org's indexed knowledge) or \`web_search\`
|
|
45
|
+
(live web). Use \`agentId\` everywhere an agent is referenced.`,
|
|
46
|
+
scopes: ["read"],
|
|
47
|
+
annotations: {
|
|
48
|
+
title: "List PipesHub agents (with toolsets & knowledge)",
|
|
49
|
+
destructiveHint: false,
|
|
50
|
+
idempotentHint: true,
|
|
51
|
+
openWorldHint: false,
|
|
52
|
+
readOnlyHint: true,
|
|
53
|
+
},
|
|
54
|
+
args,
|
|
55
|
+
tool: async (client, args, ctx) => {
|
|
56
|
+
const all = await listAllAgents(client, {
|
|
57
|
+
search: args.search,
|
|
58
|
+
signal: ctx.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!all.ok) return all.result;
|
|
61
|
+
|
|
62
|
+
return jsonResult({
|
|
63
|
+
agents: all.agents.map(trimAgent),
|
|
64
|
+
total: all.total,
|
|
65
|
+
...(all.truncated ? { truncated: true } : {}),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|