Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/toolkits/toolkits/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { spotifyClientToolkit } from "./spotify/client";
import { etsyClientToolkit } from "./etsy/client";
import { videoClientToolkit } from "./video/client";
import { twitterClientToolkit } from "./twitter/client";
import { mcpClientToolkit } from "./mcp/client";

export type ClientToolkits = {
[K in Toolkits]: ClientToolkit<
Expand All @@ -42,6 +43,7 @@ export const clientToolkits: ClientToolkits = {
[Toolkits.Etsy]: etsyClientToolkit,
[Toolkits.Video]: videoClientToolkit,
[Toolkits.Twitter]: twitterClientToolkit,
[Toolkits.Mcp]: mcpClientToolkit,
};

export function getClientToolkit<T extends Toolkits>(
Expand Down
25 changes: 25 additions & 0 deletions src/toolkits/toolkits/mcp/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ToolkitConfig } from "@/toolkits/types";
import { z } from "zod";
import { baseCallMcpTool } from "./tools/call_tool/base";
import { baseListMcpToolsTool } from "./tools/list_tools/base";
import { McpTools } from "./tools/tools";

export const mcpParameters = z.object({
url: z.string().url(),
transport: z.enum(["http", "sse"]).default("http"),
headers: z
.string()
.optional()
.describe("Optional JSON object of HTTP headers for the MCP server"),
});

export const baseMcpToolkitConfig: ToolkitConfig<
McpTools,
typeof mcpParameters.shape
> = {
tools: {
[McpTools.ListTools]: baseListMcpToolsTool,
[McpTools.CallTool]: baseCallMcpTool,
},
parameters: mcpParameters,
};
25 changes: 25 additions & 0 deletions src/toolkits/toolkits/mcp/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Network } from "lucide-react";

import { createClientToolkit } from "@/toolkits/create-toolkit";
import { ToolkitGroups } from "@/toolkits/types";
import { baseMcpToolkitConfig } from "./base";
import { McpForm } from "./form";
import { mcpCallToolConfigClient } from "./tools/call_tool/client";
import { mcpListToolsToolConfigClient } from "./tools/list_tools/client";
import { McpTools } from "./tools/tools";

export const mcpClientToolkit = createClientToolkit(
baseMcpToolkitConfig,
{
name: "MCP Server",
description: "Connect to a hosted MCP server by URL",
icon: Network,
form: McpForm,
type: ToolkitGroups.DataSource,
envVars: [],
},
{
[McpTools.ListTools]: mcpListToolsToolConfigClient,
[McpTools.CallTool]: mcpCallToolConfigClient,
},
);
65 changes: 65 additions & 0 deletions src/toolkits/toolkits/mcp/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import type React from "react";
import type { z, ZodObject } from "zod";

import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { mcpParameters } from "./base";

export const McpForm: React.ComponentType<{
parameters: z.infer<ZodObject<typeof mcpParameters.shape>>;
setParameters: (
parameters: z.infer<ZodObject<typeof mcpParameters.shape>>,
) => void;
}> = ({ parameters, setParameters }) => {
const setParameter = <K extends keyof typeof parameters>(
key: K,
value: (typeof parameters)[K],
) => {
setParameters({
...parameters,
transport: parameters.transport ?? "http",
[key]: value,
});
};

return (
<div className="space-y-4 p-4">
<div className="space-y-2">
<Label htmlFor="mcp-url">Server URL</Label>
<Input
id="mcp-url"
placeholder="https://example.com/mcp"
value={parameters.url ?? ""}
onChange={(event) => setParameter("url", event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="mcp-transport">Transport</Label>
<select
id="mcp-transport"
className="border-input bg-background h-9 w-full rounded-md border px-3 text-sm"
value={parameters.transport ?? "http"}
onChange={(event) =>
setParameter("transport", event.target.value as "http" | "sse")
}
>
<option value="http">Streamable HTTP</option>
<option value="sse">Server-sent events</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="mcp-headers">Headers JSON</Label>
<Textarea
id="mcp-headers"
placeholder='{"Authorization":"Bearer ..."}'
value={parameters.headers ?? ""}
onChange={(event) => setParameter("headers", event.target.value)}
className="min-h-24 font-mono text-xs"
/>
</div>
</div>
);
};
19 changes: 19 additions & 0 deletions src/toolkits/toolkits/mcp/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createServerToolkit } from "@/toolkits/create-toolkit";
import { baseMcpToolkitConfig } from "./base";
import { mcpCallToolConfigServer } from "./tools/call_tool/server";
import { mcpListToolsToolConfigServer } from "./tools/list_tools/server";
import { McpTools } from "./tools/tools";
import { createMcpClientConfig } from "./transport";

export const mcpToolkitServer = createServerToolkit(
baseMcpToolkitConfig,
`You have access to a user-configured hosted MCP server. Use "List MCP tools" first to discover the server's available tools, then call "Call MCP tool" with the exact tool name and JSON arguments.`,
async (params) => {
const config = createMcpClientConfig(params);

return {
[McpTools.ListTools]: mcpListToolsToolConfigServer(config),
[McpTools.CallTool]: mcpCallToolConfigServer(config),
};
},
);
16 changes: 16 additions & 0 deletions src/toolkits/toolkits/mcp/tools/call_tool/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from "zod";

import { createBaseTool } from "@/toolkits/create-tool";

export const baseCallMcpTool = createBaseTool({
description: "Call one tool exposed by the configured MCP server.",
inputSchema: z.object({
toolName: z.string().min(1),
arguments: z.record(z.any()).default({}),
}),
outputSchema: z.object({
content: z.unknown(),
structuredContent: z.unknown().optional(),
isError: z.boolean().optional(),
}),
});
25 changes: 25 additions & 0 deletions src/toolkits/toolkits/mcp/tools/call_tool/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Network } from "lucide-react";

import type { ClientToolConfig } from "@/toolkits/types";
import { CodeBlock } from "@/components/ui/code-block";
import type { baseCallMcpTool } from "./base";

export const mcpCallToolConfigClient: ClientToolConfig<
typeof baseCallMcpTool.inputSchema.shape,
typeof baseCallMcpTool.outputSchema.shape
> = {
CallComponent: ({ args }) => (
<div className="text-muted-foreground flex items-center gap-2">
<Network className="size-4" />
<span className="text-sm">
Calling MCP tool: {args.toolName ?? "..."}
</span>
</div>
),
ResultComponent: ({ result }) => (
<div className="space-y-2">
<h1 className="text-muted-foreground text-sm font-medium">MCP Result</h1>
<CodeBlock language="json" value={JSON.stringify(result, null, 2)} />
</div>
),
};
29 changes: 29 additions & 0 deletions src/toolkits/toolkits/mcp/tools/call_tool/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ServerToolConfig } from "@/toolkits/types";
import type { baseCallMcpTool } from "./base";
import type { createMcpClientConfig } from "../../transport";
import { withMcpClient } from "../../transport";

export const mcpCallToolConfigServer = (
config: ReturnType<typeof createMcpClientConfig>,
): ServerToolConfig<
typeof baseCallMcpTool.inputSchema.shape,
typeof baseCallMcpTool.outputSchema.shape
> => ({
callback: async ({ toolName, arguments: toolArguments }) => {
return await withMcpClient(config, async (client) => {
const response = await client.callTool({
name: toolName,
arguments: toolArguments,
});

return {
content: response.content,
structuredContent: response.structuredContent,
isError:
typeof response.isError === "boolean" ? response.isError : undefined,
};
});
},
message:
"The user is shown the MCP tool result. Use the structured result to answer their request.",
});
17 changes: 17 additions & 0 deletions src/toolkits/toolkits/mcp/tools/list_tools/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";

import { createBaseTool } from "@/toolkits/create-tool";

export const baseListMcpToolsTool = createBaseTool({
description: "List the tools exposed by the configured MCP server.",
inputSchema: z.object({}),
outputSchema: z.object({
tools: z.array(
z.object({
name: z.string(),
description: z.string().optional(),
inputSchema: z.unknown().optional(),
}),
),
}),
});
32 changes: 32 additions & 0 deletions src/toolkits/toolkits/mcp/tools/list_tools/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ListTree } from "lucide-react";

import type { ClientToolConfig } from "@/toolkits/types";
import { CodeBlock } from "@/components/ui/code-block";
import type { baseListMcpToolsTool } from "./base";

export const mcpListToolsToolConfigClient: ClientToolConfig<
typeof baseListMcpToolsTool.inputSchema.shape,
typeof baseListMcpToolsTool.outputSchema.shape
> = {
CallComponent: () => (
<div className="text-muted-foreground flex items-center gap-2">
<ListTree className="size-4" />
<span className="text-sm">Listing MCP tools</span>
</div>
),
ResultComponent: ({ result }) => {
if (!result.tools.length) {
return <div className="text-muted-foreground">No MCP tools found</div>;
}

return (
<div className="space-y-2">
<h1 className="text-muted-foreground text-sm font-medium">MCP Tools</h1>
<CodeBlock
language="json"
value={JSON.stringify(result.tools, null, 2)}
/>
</div>
);
},
};
27 changes: 27 additions & 0 deletions src/toolkits/toolkits/mcp/tools/list_tools/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ServerToolConfig } from "@/toolkits/types";
import type { baseListMcpToolsTool } from "./base";
import type { createMcpClientConfig } from "../../transport";
import { withMcpClient } from "../../transport";

export const mcpListToolsToolConfigServer = (
config: ReturnType<typeof createMcpClientConfig>,
): ServerToolConfig<
typeof baseListMcpToolsTool.inputSchema.shape,
typeof baseListMcpToolsTool.outputSchema.shape
> => ({
callback: async () => {
return await withMcpClient(config, async (client) => {
const response = await client.listTools();

return {
tools: response.tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
},
message:
"The user is shown the MCP server's tools. Use the exact tool names when calling one of them.",
});
4 changes: 4 additions & 0 deletions src/toolkits/toolkits/mcp/tools/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum McpTools {
ListTools = "list-tools",
CallTool = "call-tool",
}
59 changes: 59 additions & 0 deletions src/toolkits/toolkits/mcp/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { z } from "zod";

import type { mcpParameters } from "./base";

export type McpClientConfig = z.infer<typeof mcpParameters>;

export const createMcpClientConfig = (params: McpClientConfig) => {
return {
...params,
headers: parseHeaders(params.headers),
};
};

const parseHeaders = (headers?: string): Record<string, string> | undefined => {
if (!headers?.trim()) {
return undefined;
}

const parsed = JSON.parse(headers) as unknown;

if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("MCP headers must be a JSON object");
}

return Object.fromEntries(
Object.entries(parsed).map(([key, value]) => [key, String(value)]),
);
};

export const withMcpClient = async <T>(
config: ReturnType<typeof createMcpClientConfig>,
callback: (client: Client) => Promise<T>,
) => {
const client = new Client({
name: "toolkit-dev-mcp-client",
version: "0.1.0",
});

const url = new URL(config.url);
const transport =
config.transport === "sse"
? new SSEClientTransport(url, {
requestInit: { headers: config.headers },
})
: new StreamableHTTPClientTransport(url, {
requestInit: { headers: config.headers },
});

await client.connect(transport);

try {
return await callback(client);
} finally {
await client.close();
}
};
2 changes: 2 additions & 0 deletions src/toolkits/toolkits/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { spotifyToolkitServer } from "./spotify/server";
import { etsyToolkitServer } from "./etsy/server";
import { videoToolkitServer } from "./video/server";
import { twitterToolkitServer } from "./twitter/server";
import { mcpToolkitServer } from "./mcp/server";
import {
Toolkits,
type ServerToolkitNames,
Expand Down Expand Up @@ -41,6 +42,7 @@ export const serverToolkits: ServerToolkits = {
[Toolkits.Etsy]: etsyToolkitServer,
[Toolkits.Video]: videoToolkitServer,
[Toolkits.Twitter]: twitterToolkitServer,
[Toolkits.Mcp]: mcpToolkitServer,
};

export function getServerToolkit<T extends Toolkits>(
Expand Down
Loading