-
Notifications
You must be signed in to change notification settings - Fork 43
feat(connections): add connection auth tools and inline auth card UI #2769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
c447fb3
feat(connections): add connection auth tools and inline auth card UI
tlgimenes 27f05aa
feat(connections): unify install flow with CONNECTION_INSTALL for UI …
tlgimenes af0e3b0
fix(decopilot): rename "mesh" MCP server key to "agent" in Claude Cod…
tlgimenes e62a386
fix(types): replace ToolSchema with inline Zod schema for JSON Schema…
tlgimenes cb0fd06
feat(virtual-mcp): namespace colliding tool names as connection-slug:…
tlgimenes c31bbd2
feat(virtual-mcp): always namespace downstream tools with connection …
tlgimenes c07039f
fix(plugins): exclude plugin tools when no plugins are enabled
tlgimenes 5a44489
feat(decopilot): add builtin MCP server for Claude Code and fix regis…
tlgimenes 53802ec
fix(chat): match namespaced connection auth tool names for auth card …
tlgimenes f061666
docs(guides): update store and connection prompts to use CONNECTION_I…
tlgimenes 9e97d48
fix(web): remove missing ORG_ADMIN_PROJECT_SLUG references
tlgimenes ff97842
fix(web): remove unused useConnectionActions import
tlgimenes fde9fd9
merge: resolve conflicts from origin/main
tlgimenes 23dc83e
merge: resolve conflicts from origin/main
tlgimenes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
246 changes: 246 additions & 0 deletions
246
apps/mesh/src/api/routes/decopilot/built-in-tools/built-in-mcp-server.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| import { createSdkMcpServer, tool } from "ai-sdk-provider-claude-code"; | ||
| import { z } from "zod"; | ||
| import type { VirtualClient } from "./sandbox"; | ||
| import { normalizePromptContent } from "./prompts"; | ||
| import { | ||
| MAX_RESULT_TOKENS, | ||
| createOutputPreview, | ||
| estimateJsonTokens, | ||
| } from "./read-tool-output"; | ||
|
|
||
| export function createBuiltinMcpServer( | ||
| passthroughClient: VirtualClient, | ||
| toolOutputMap: Map<string, string>, | ||
| ) { | ||
| const readPrompt = tool( | ||
| "read_prompt", | ||
| "Read a prompt by name from <available-prompts>. " + | ||
| "Returns the prompt messages with action-oriented guide content. " + | ||
| "Use this to load step-by-step instructions for common tasks.", | ||
| { | ||
| name: z | ||
| .string() | ||
| .min(1) | ||
| .describe("The name of the prompt from <available-prompts>."), | ||
| arguments: z | ||
| .record(z.string(), z.string()) | ||
| .optional() | ||
| .describe( | ||
| "Optional arguments for the prompt, as key-value string pairs.", | ||
| ), | ||
| }, | ||
| async ({ name, arguments: args }) => { | ||
| try { | ||
| const result = await passthroughClient.getPrompt({ | ||
| name, | ||
| arguments: args, | ||
| }); | ||
| const messages = result.messages; | ||
|
|
||
| if (!messages || messages.length === 0) { | ||
| return { | ||
| content: [ | ||
| { type: "text" as const, text: "Prompt returned no content." }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| const parts = messages.map((m) => ({ | ||
| role: m.role, | ||
| content: normalizePromptContent(m.content), | ||
| })); | ||
|
|
||
| const serialized = JSON.stringify(parts, null, 2); | ||
| const tokens = estimateJsonTokens(serialized); | ||
|
|
||
| if (tokens > MAX_RESULT_TOKENS) { | ||
| const toolCallId = `prompt_${Date.now()}`; | ||
| toolOutputMap.set(toolCallId, serialized); | ||
| const preview = createOutputPreview(serialized); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Prompt content too large (${tokens} tokens). Use read_tool_output with tool_call_id "${toolCallId}" to extract specific data.\n\nPreview:\n${preview}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| return { content: [{ type: "text" as const, text: serialized }] }; | ||
| } catch (error) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Error reading prompt: ${error instanceof Error ? error.message : String(error)}`, | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| const readResource = tool( | ||
| "read_resource", | ||
| "Read a resource by its URI. Returns the content of the resource. " + | ||
| "Resource URIs (docs://...) are provided in prompt content.", | ||
| { | ||
| uri: z | ||
| .string() | ||
| .min(1) | ||
| .describe("The URI of the resource to read (e.g. docs://store.md)."), | ||
| }, | ||
| async ({ uri }) => { | ||
| try { | ||
| const result = await passthroughClient.readResource({ uri }); | ||
| const contents = result.contents; | ||
|
|
||
| if (!contents || contents.length === 0) { | ||
| return { | ||
| content: [ | ||
| { type: "text" as const, text: "Resource returned no content." }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| const parts = contents.map((c) => { | ||
| if ("text" in c && c.text !== undefined) { | ||
| return { uri: c.uri, mimeType: c.mimeType, text: c.text }; | ||
| } | ||
| if ("blob" in c && c.blob !== undefined) { | ||
| return { | ||
| uri: c.uri, | ||
| mimeType: c.mimeType, | ||
| blob: `[binary data, ${c.blob.length} bytes base64]`, | ||
| }; | ||
| } | ||
| return { uri: c.uri, mimeType: c.mimeType }; | ||
| }); | ||
|
|
||
| const serialized = JSON.stringify(parts, null, 2); | ||
| const tokens = estimateJsonTokens(serialized); | ||
|
|
||
| if (tokens > MAX_RESULT_TOKENS) { | ||
| const toolCallId = `resource_${Date.now()}`; | ||
| toolOutputMap.set(toolCallId, serialized); | ||
| const preview = createOutputPreview(serialized); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Resource content too large (${tokens} tokens). Use read_tool_output with tool_call_id "${toolCallId}" to extract specific data.\n\nPreview:\n${preview}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| return { content: [{ type: "text" as const, text: serialized }] }; | ||
| } catch (error) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Error reading resource: ${error instanceof Error ? error.message : String(error)}`, | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| const readToolOutput = tool( | ||
| "read_tool_output", | ||
| "Filter a tool output that was too large to display inline. " + | ||
| "Returns all lines matching the given regular expression pattern (grep-like). " + | ||
| "You may call this tool multiple times with different patterns to extract different pieces of information.", | ||
| { | ||
| tool_call_id: z | ||
| .string() | ||
| .describe("The tool call ID from the truncated output."), | ||
| pattern: z | ||
| .string() | ||
| .min(1) | ||
| .describe( | ||
| "Regular expression pattern to filter tool output lines. Returns all matching lines.", | ||
| ), | ||
| }, | ||
| async ({ tool_call_id, pattern }) => { | ||
| try { | ||
| if (!toolOutputMap.has(tool_call_id)) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Tool output not found for tool call id: ${tool_call_id}. Available ids: ${[...toolOutputMap.keys()].join(", ") || "(none)"}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| const input = toolOutputMap.get(tool_call_id)!; | ||
|
|
||
| let regex: RegExp; | ||
| try { | ||
| regex = new RegExp(pattern); | ||
| } catch { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Invalid regex pattern: ${pattern}`, | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
|
|
||
| const lines = input.split("\n"); | ||
| const matching = lines.filter((line) => regex.test(line)); | ||
| const resultText = matching.join("\n"); | ||
|
|
||
| const tokenCount = estimateJsonTokens(resultText); | ||
| if (tokenCount > MAX_RESULT_TOKENS) { | ||
| const preview = createOutputPreview(resultText); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Output is still too long (${tokenCount} tokens), use a more specific pattern to reduce output.\n\nPreview:\n${preview}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: JSON.stringify({ | ||
| result: resultText, | ||
| matchCount: matching.length, | ||
| totalLines: lines.length, | ||
| }), | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Error filtering tool output: ${error instanceof Error ? error.message : String(error)}`, | ||
| }, | ||
| ], | ||
| isError: true, | ||
| }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| return createSdkMcpServer({ | ||
| name: "builtins", | ||
| tools: [readPrompt, readResource, readToolOutput], | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Tool output IDs use
Date.now(), which can collide under concurrency and overwrite stored outputs.Prompt for AI agents