feat(mcp): add Zod schema validation to all tool handlers
Add src/utils/validation.ts with validate() helper and InvalidArgumentsError. Update all tool files (agents, approvals, discovery, files, plugins, secrets) to use explicit Zod schemas with .describe() for tool input validation. Every handler now calls validate(args, Schema) before any business logic, throwing INVALID_ARGUMENTS (MCP error -32602) on schema failure. Also: add node_modules/, dist/, build/ to .gitignore; update package-lock.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
73efb00f48
commit
925ecc1f26
5
.gitignore
vendored
5
.gitignore
vendored
@ -19,3 +19,8 @@
|
|||||||
# Workspace auth tokens
|
# Workspace auth tokens
|
||||||
.auth-token
|
.auth-token
|
||||||
.auth_token
|
.auth_token
|
||||||
|
|
||||||
|
# Node.js dependencies (installed at runtime, not part of repo)
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|||||||
11
package-lock.json
generated
11
package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@molecule/mcp-server",
|
"name": "@molecule-ai/mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@molecule/mcp-server",
|
"name": "@molecule-ai/mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
@ -53,7 +53,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@ -1809,7 +1808,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
@ -2482,7 +2480,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@ -2861,7 +2858,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz",
|
||||||
"integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==",
|
"integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
}
|
}
|
||||||
@ -3143,7 +3139,6 @@
|
|||||||
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
|
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.3.0",
|
"@jest/core": "30.3.0",
|
||||||
"@jest/types": "30.3.0",
|
"@jest/types": "30.3.0",
|
||||||
@ -5156,7 +5151,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -5541,7 +5535,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,67 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, isApiError, platformGet, PLATFORM_URL, toMcpResult, toMcpText } from "../api.js";
|
import { apiCall, toMcpResult, toMcpText } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
import { platformGet } from "../api.js";
|
||||||
|
|
||||||
export async function handleChatWithAgent(params: { workspace_id: string; message: string }) {
|
// ---------------------------------------------------------------------------
|
||||||
const { workspace_id, message } = params;
|
// Schemas
|
||||||
const data = await apiCall<{ result?: { parts?: Array<{ kind?: string; text?: string }> } }>(
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ChatWithAgentSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
message: z.string().describe("Message to send"),
|
||||||
|
});
|
||||||
|
export type ChatWithAgentParams = z.infer<typeof ChatWithAgentSchema>;
|
||||||
|
|
||||||
|
const AssignAgentSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
model: z.string().describe("Model string (e.g., openrouter:anthropic/claude-3.5-haiku)"),
|
||||||
|
});
|
||||||
|
export type AssignAgentParams = z.infer<typeof AssignAgentSchema>;
|
||||||
|
|
||||||
|
const ReplaceAgentSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
model: z.string().describe("Model string"),
|
||||||
|
});
|
||||||
|
export type ReplaceAgentParams = z.infer<typeof ReplaceAgentSchema>;
|
||||||
|
|
||||||
|
const RemoveAgentSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type RemoveAgentParams = z.infer<typeof RemoveAgentSchema>;
|
||||||
|
|
||||||
|
const MoveAgentSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Source workspace ID"),
|
||||||
|
target_workspace_id: z.string().describe("Target workspace ID"),
|
||||||
|
});
|
||||||
|
export type MoveAgentParams = z.infer<typeof MoveAgentSchema>;
|
||||||
|
|
||||||
|
const GetModelSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type GetModelParams = z.infer<typeof GetModelSchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleChatWithAgent(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ChatWithAgentSchema);
|
||||||
|
const data = await apiCall<
|
||||||
|
{ result?: { parts?: Array<{ kind?: string; text?: string }> } }
|
||||||
|
>(
|
||||||
"POST",
|
"POST",
|
||||||
`/workspaces/${workspace_id}/a2a`,
|
`/workspaces/${params.workspace_id}/a2a`,
|
||||||
{
|
{
|
||||||
method: "message/send",
|
method: "message/send",
|
||||||
params: {
|
params: {
|
||||||
message: { role: "user", parts: [{ type: "text", text: message }] },
|
message: { role: "user", parts: [{ type: "text", text: params.message }] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const parts = (data as { result?: { parts?: Array<{ kind?: string; text?: string }> } } | null)?.result?.parts || [];
|
const parts =
|
||||||
|
(data as { result?: { parts?: Array<{ kind?: string; text?: string }> } } | null)?.result?.parts || [];
|
||||||
const text = parts
|
const text = parts
|
||||||
.filter((p) => p.kind === "text")
|
.filter((p) => p.kind === "text")
|
||||||
.map((p) => p.text || "")
|
.map((p) => p.text || "")
|
||||||
@ -22,34 +69,42 @@ export async function handleChatWithAgent(params: { workspace_id: string; messag
|
|||||||
return text ? toMcpText(text) : toMcpResult(data);
|
return text ? toMcpText(text) : toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleAssignAgent(params: { workspace_id: string; model: string }) {
|
export async function handleAssignAgent(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, model } = params;
|
const params = validate(args, AssignAgentSchema);
|
||||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/agent`, { model });
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/agent`, { model: params.model });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleReplaceAgent(params: { workspace_id: string; model: string }) {
|
export async function handleReplaceAgent(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, model } = params;
|
const params = validate(args, ReplaceAgentSchema);
|
||||||
const data = await apiCall("PATCH", `/workspaces/${workspace_id}/agent`, { model });
|
const data = await apiCall("PATCH", `/workspaces/${params.workspace_id}/agent`, { model: params.model });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRemoveAgent(params: { workspace_id: string }) {
|
export async function handleRemoveAgent(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, RemoveAgentSchema);
|
||||||
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/agent`);
|
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/agent`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleMoveAgent(params: { workspace_id: string; target_workspace_id: string }) {
|
export async function handleMoveAgent(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, target_workspace_id } = params;
|
const params = validate(args, MoveAgentSchema);
|
||||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/agent/move`, { target_workspace_id });
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/agent/move`, {
|
||||||
|
target_workspace_id: params.target_workspace_id,
|
||||||
|
});
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGetModel(params: { workspace_id: string }) {
|
export async function handleGetModel(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, GetModelSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/model`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/model`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerAgentTools(srv: McpServer) {
|
export function registerAgentTools(srv: McpServer) {
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"chat_with_agent",
|
"chat_with_agent",
|
||||||
@ -74,28 +129,31 @@ export function registerAgentTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"replace_agent",
|
"replace_agent",
|
||||||
"Replace the model on an existing workspace agent",
|
"Replace the model on an existing workspace agent",
|
||||||
{ workspace_id: z.string(), model: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID"), model: z.string().describe("Model string") },
|
||||||
handleReplaceAgent
|
handleReplaceAgent
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"remove_agent",
|
"remove_agent",
|
||||||
"Remove the agent from a workspace",
|
"Remove the agent from a workspace",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleRemoveAgent
|
handleRemoveAgent
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"move_agent",
|
"move_agent",
|
||||||
"Move an agent from one workspace to another",
|
"Move an agent from one workspace to another",
|
||||||
{ workspace_id: z.string(), target_workspace_id: z.string() },
|
{
|
||||||
|
workspace_id: z.string().describe("Source workspace ID"),
|
||||||
|
target_workspace_id: z.string().describe("Target workspace ID"),
|
||||||
|
},
|
||||||
handleMoveAgent
|
handleMoveAgent
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"get_model",
|
"get_model",
|
||||||
"Get current model configuration for a workspace",
|
"Get current model configuration for a workspace",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleGetModel
|
handleGetModel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,70 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
|
||||||
export async function handleListPendingApprovals() {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schemas
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const DecideApprovalSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
approval_id: z.string().describe("Approval ID"),
|
||||||
|
decision: z.enum(["approved", "denied"]).describe("Decision"),
|
||||||
|
});
|
||||||
|
export type DecideApprovalParams = z.infer<typeof DecideApprovalSchema>;
|
||||||
|
|
||||||
|
const CreateApprovalSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
action: z.string().describe("What needs approval"),
|
||||||
|
reason: z.string().optional().describe("Why it's needed"),
|
||||||
|
});
|
||||||
|
export type CreateApprovalParams = z.infer<typeof CreateApprovalSchema>;
|
||||||
|
|
||||||
|
const GetWorkspaceApprovalsSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type GetWorkspaceApprovalsParams = z.infer<typeof GetWorkspaceApprovalsSchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleListPendingApprovals(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/approvals/pending");
|
const data = await platformGet("/approvals/pending");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDecideApproval(params: {
|
export async function handleDecideApproval(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
workspace_id: string;
|
const params = validate(args, DecideApprovalSchema);
|
||||||
approval_id: string;
|
|
||||||
decision: "approved" | "denied";
|
|
||||||
}) {
|
|
||||||
const { workspace_id, approval_id, decision } = params;
|
|
||||||
const data = await apiCall(
|
const data = await apiCall(
|
||||||
"POST",
|
"POST",
|
||||||
`/workspaces/${workspace_id}/approvals/${approval_id}/decide`,
|
`/workspaces/${params.workspace_id}/approvals/${params.approval_id}/decide`,
|
||||||
{ decision, decided_by: "mcp-client" }
|
{ decision: params.decision, decided_by: "mcp-client" }
|
||||||
);
|
);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleCreateApproval(params: {
|
export async function handleCreateApproval(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
workspace_id: string;
|
const params = validate(args, CreateApprovalSchema);
|
||||||
action: string;
|
const data = await apiCall(
|
||||||
reason?: string;
|
"POST",
|
||||||
}) {
|
`/workspaces/${params.workspace_id}/approvals`,
|
||||||
const { workspace_id, action, reason } = params;
|
{ action: params.action, reason: params.reason }
|
||||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/approvals`, { action, reason });
|
);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGetWorkspaceApprovals(params: { workspace_id: string }) {
|
export async function handleGetWorkspaceApprovals(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, GetWorkspaceApprovalsSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/approvals`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/approvals`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerApprovalTools(srv: McpServer) {
|
export function registerApprovalTools(srv: McpServer) {
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"list_pending_approvals",
|
"list_pending_approvals",
|
||||||
@ -59,7 +88,7 @@ export function registerApprovalTools(srv: McpServer) {
|
|||||||
"create_approval",
|
"create_approval",
|
||||||
"Create an approval request for a workspace",
|
"Create an approval request for a workspace",
|
||||||
{
|
{
|
||||||
workspace_id: z.string(),
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
action: z.string().describe("What needs approval"),
|
action: z.string().describe("What needs approval"),
|
||||||
reason: z.string().optional().describe("Why it's needed"),
|
reason: z.string().optional().describe("Why it's needed"),
|
||||||
},
|
},
|
||||||
@ -69,7 +98,7 @@ export function registerApprovalTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"get_workspace_approvals",
|
"get_workspace_approvals",
|
||||||
"List approval requests for a specific workspace",
|
"List approval requests for a specific workspace",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleGetWorkspaceApprovals
|
handleGetWorkspaceApprovals
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,99 +1,183 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
|
||||||
export async function handleListPeers(params: { workspace_id: string }) {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schemas
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ListPeersSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ListPeersParams = z.infer<typeof ListPeersSchema>;
|
||||||
|
|
||||||
|
const DiscoverWorkspaceSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type DiscoverWorkspaceParams = z.infer<typeof DiscoverWorkspaceSchema>;
|
||||||
|
|
||||||
|
const CheckAccessSchema = z.object({
|
||||||
|
caller_id: z.string().describe("Caller workspace ID"),
|
||||||
|
target_id: z.string().describe("Target workspace ID"),
|
||||||
|
});
|
||||||
|
export type CheckAccessParams = z.infer<typeof CheckAccessSchema>;
|
||||||
|
|
||||||
|
const ListEventsSchema = z.object({
|
||||||
|
workspace_id: z.string().optional().describe("Filter to workspace, or omit for all"),
|
||||||
|
});
|
||||||
|
export type ListEventsParams = z.infer<typeof ListEventsSchema>;
|
||||||
|
|
||||||
|
const ImportOrgSchema = z.object({
|
||||||
|
dir: z.string().describe("Org template directory name (e.g., 'molecule-dev')"),
|
||||||
|
});
|
||||||
|
export type ImportOrgParams = z.infer<typeof ImportOrgSchema>;
|
||||||
|
|
||||||
|
const ImportTemplateSchema = z.object({
|
||||||
|
name: z.string().describe("Template name"),
|
||||||
|
files: z.record(z.string()).describe("Map of file path → content"),
|
||||||
|
});
|
||||||
|
export type ImportTemplateParams = z.infer<typeof ImportTemplateSchema>;
|
||||||
|
|
||||||
|
const ExportBundleSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ExportBundleParams = z.infer<typeof ExportBundleSchema>;
|
||||||
|
|
||||||
|
const ImportBundleSchema = z.object({
|
||||||
|
bundle: z.record(z.unknown()).describe("Bundle JSON object"),
|
||||||
|
});
|
||||||
|
export type ImportBundleParams = z.infer<typeof ImportBundleSchema>;
|
||||||
|
|
||||||
|
const SetViewportSchema = z.object({
|
||||||
|
x: z.number().describe("Viewport X coordinate"),
|
||||||
|
y: z.number().describe("Viewport Y coordinate"),
|
||||||
|
zoom: z.number().describe("Zoom level"),
|
||||||
|
});
|
||||||
|
export type SetViewportParams = z.infer<typeof SetViewportSchema>;
|
||||||
|
|
||||||
|
const ExpandTeamSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID to expand"),
|
||||||
|
});
|
||||||
|
export type ExpandTeamParams = z.infer<typeof ExpandTeamSchema>;
|
||||||
|
|
||||||
|
const CollapseTeamSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID to collapse"),
|
||||||
|
});
|
||||||
|
export type CollapseTeamParams = z.infer<typeof CollapseTeamSchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleListPeers(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListPeersSchema);
|
||||||
const data = await platformGet(`/registry/${params.workspace_id}/peers`);
|
const data = await platformGet(`/registry/${params.workspace_id}/peers`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDiscoverWorkspace(params: { workspace_id: string }) {
|
export async function handleDiscoverWorkspace(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, DiscoverWorkspaceSchema);
|
||||||
const data = await platformGet(`/registry/discover/${params.workspace_id}`);
|
const data = await platformGet(`/registry/discover/${params.workspace_id}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleCheckAccess(params: { caller_id: string; target_id: string }) {
|
export async function handleCheckAccess(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { caller_id, target_id } = params;
|
const params = validate(args, CheckAccessSchema);
|
||||||
const data = await apiCall("POST", `/registry/check-access`, { caller_id, target_id });
|
const data = await apiCall("POST", `/registry/check-access`, { caller_id: params.caller_id, target_id: params.target_id });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListEvents(params: { workspace_id?: string }) {
|
export async function handleListEvents(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListEventsSchema);
|
||||||
const path = params.workspace_id ? `/events/${params.workspace_id}` : "/events";
|
const path = params.workspace_id ? `/events/${params.workspace_id}` : "/events";
|
||||||
const data = await platformGet(path);
|
const data = await platformGet(path);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListTemplates() {
|
export async function handleListTemplates(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/templates");
|
const data = await platformGet("/templates");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListOrgTemplates() {
|
export async function handleListOrgTemplates(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/org/templates");
|
const data = await platformGet("/org/templates");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleImportOrg(params: { dir: string }) {
|
export async function handleImportOrg(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ImportOrgSchema);
|
||||||
const data = await apiCall("POST", "/org/import", { dir: params.dir });
|
const data = await apiCall("POST", "/org/import", { dir: params.dir });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleImportTemplate(params: { name: string; files: Record<string, string> }) {
|
export async function handleImportTemplate(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { name, files } = params;
|
const params = validate(args, ImportTemplateSchema);
|
||||||
const data = await apiCall("POST", `/templates/import`, { name, files });
|
const data = await apiCall("POST", `/templates/import`, { name: params.name, files: params.files });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExportBundle(params: { workspace_id: string }) {
|
export async function handleExportBundle(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ExportBundleSchema);
|
||||||
const data = await platformGet(`/bundles/export/${params.workspace_id}`);
|
const data = await platformGet(`/bundles/export/${params.workspace_id}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleImportBundle(params: { bundle: Record<string, unknown> }) {
|
export async function handleImportBundle(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ImportBundleSchema);
|
||||||
const data = await apiCall("POST", `/bundles/import`, params.bundle);
|
const data = await apiCall("POST", `/bundles/import`, params.bundle);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGetViewport() {
|
export async function handleGetViewport(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/canvas/viewport");
|
const data = await platformGet("/canvas/viewport");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSetViewport(params: { x: number; y: number; zoom: number }) {
|
export async function handleSetViewport(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, SetViewportSchema);
|
||||||
const data = await apiCall("PUT", "/canvas/viewport", params);
|
const data = await apiCall("PUT", "/canvas/viewport", params);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExpandTeam(params: { workspace_id: string }) {
|
export async function handleExpandTeam(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ExpandTeamSchema);
|
||||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/expand`, {});
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/expand`, {});
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleCollapseTeam(params: { workspace_id: string }) {
|
export async function handleCollapseTeam(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, CollapseTeamSchema);
|
||||||
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/collapse`, {});
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/collapse`, {});
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerDiscoveryTools(srv: McpServer) {
|
export function registerDiscoveryTools(srv: McpServer) {
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"list_peers",
|
"list_peers",
|
||||||
"List reachable peer workspaces (siblings, children, parent)",
|
"List reachable peer workspaces (siblings, children, parent)",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleListPeers
|
handleListPeers
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"discover_workspace",
|
"discover_workspace",
|
||||||
"Resolve a workspace URL by ID (for A2A communication)",
|
"Resolve a workspace URL by ID (for A2A communication)",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleDiscoverWorkspace
|
handleDiscoverWorkspace
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"check_access",
|
"check_access",
|
||||||
"Check if two workspaces can communicate",
|
"Check if two workspaces can communicate",
|
||||||
{ caller_id: z.string(), target_id: z.string() },
|
{
|
||||||
|
caller_id: z.string().describe("Caller workspace ID"),
|
||||||
|
target_id: z.string().describe("Target workspace ID"),
|
||||||
|
},
|
||||||
handleCheckAccess
|
handleCheckAccess
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -128,7 +212,7 @@ export function registerDiscoveryTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"export_bundle",
|
"export_bundle",
|
||||||
"Export a workspace as a portable .bundle.json",
|
"Export a workspace as a portable .bundle.json",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleExportBundle
|
handleExportBundle
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -150,9 +234,9 @@ export function registerDiscoveryTools(srv: McpServer) {
|
|||||||
"set_canvas_viewport",
|
"set_canvas_viewport",
|
||||||
"Persist the canvas viewport (x, y, zoom).",
|
"Persist the canvas viewport (x, y, zoom).",
|
||||||
{
|
{
|
||||||
x: z.number(),
|
x: z.number().describe("Viewport X coordinate"),
|
||||||
y: z.number(),
|
y: z.number().describe("Viewport Y coordinate"),
|
||||||
zoom: z.number(),
|
zoom: z.number().describe("Zoom level"),
|
||||||
},
|
},
|
||||||
handleSetViewport,
|
handleSetViewport,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,51 +1,104 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, platformGet, toMcpResult, toMcpText } from "../api.js";
|
import { apiCall, platformGet, toMcpResult, toMcpText } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
|
||||||
export async function handleListFiles(params: { workspace_id: string }) {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schemas
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ListFilesSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ListFilesParams = z.infer<typeof ListFilesSchema>;
|
||||||
|
|
||||||
|
const ReadFileSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
path: z.string().describe("File path (e.g., system-prompt.md, skills/seo/SKILL.md)"),
|
||||||
|
});
|
||||||
|
export type ReadFileParams = z.infer<typeof ReadFileSchema>;
|
||||||
|
|
||||||
|
const WriteFileSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
path: z.string().describe("File path"),
|
||||||
|
content: z.string().describe("File content"),
|
||||||
|
});
|
||||||
|
export type WriteFileParams = z.infer<typeof WriteFileSchema>;
|
||||||
|
|
||||||
|
const DeleteFileSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
path: z.string().describe("File or folder path"),
|
||||||
|
});
|
||||||
|
export type DeleteFileParams = z.infer<typeof DeleteFileSchema>;
|
||||||
|
|
||||||
|
const ReplaceAllFilesSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
files: z.record(z.string()).describe("Map of file path → content"),
|
||||||
|
});
|
||||||
|
export type ReplaceAllFilesParams = z.infer<typeof ReplaceAllFilesSchema>;
|
||||||
|
|
||||||
|
const GetConfigSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type GetConfigParams = z.infer<typeof GetConfigSchema>;
|
||||||
|
|
||||||
|
const UpdateConfigSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
config: z.record(z.unknown()).describe("Config fields to update"),
|
||||||
|
});
|
||||||
|
export type UpdateConfigParams = z.infer<typeof UpdateConfigSchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleListFiles(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListFilesSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/files`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/files`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleReadFile(params: { workspace_id: string; path: string }) {
|
export async function handleReadFile(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, path } = params;
|
const params = validate(args, ReadFileSchema);
|
||||||
const data = await platformGet<{ content?: string }>(`/workspaces/${workspace_id}/files/${path}`);
|
const data = await platformGet<{ content?: string }>(`/workspaces/${params.workspace_id}/files/${params.path}`);
|
||||||
const fileText = (data as { content?: string } | null)?.content;
|
const fileText = (data as { content?: string } | null)?.content;
|
||||||
return fileText ? toMcpText(fileText) : toMcpResult(data);
|
return fileText ? toMcpText(fileText) : toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleWriteFile(params: { workspace_id: string; path: string; content: string }) {
|
export async function handleWriteFile(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, path, content } = params;
|
const params = validate(args, WriteFileSchema);
|
||||||
const data = await apiCall("PUT", `/workspaces/${workspace_id}/files/${path}`, { content });
|
const data = await apiCall("PUT", `/workspaces/${params.workspace_id}/files/${params.path}`, { content: params.content });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDeleteFile(params: { workspace_id: string; path: string }) {
|
export async function handleDeleteFile(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, path } = params;
|
const params = validate(args, DeleteFileSchema);
|
||||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/files/${path}`);
|
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/files/${params.path}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleReplaceAllFiles(params: {
|
export async function handleReplaceAllFiles(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
workspace_id: string;
|
const params = validate(args, ReplaceAllFilesSchema);
|
||||||
files: Record<string, string>;
|
const data = await apiCall("PUT", `/workspaces/${params.workspace_id}/files`, { files: params.files });
|
||||||
}) {
|
|
||||||
const { workspace_id, files } = params;
|
|
||||||
const data = await apiCall("PUT", `/workspaces/${workspace_id}/files`, { files });
|
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGetConfig(params: { workspace_id: string }) {
|
export async function handleGetConfig(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, GetConfigSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/config`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/config`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleUpdateConfig(params: { workspace_id: string; config: Record<string, unknown> }) {
|
export async function handleUpdateConfig(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, config } = params;
|
const params = validate(args, UpdateConfigSchema);
|
||||||
const data = await apiCall("PATCH", `/workspaces/${workspace_id}/config`, config);
|
const data = await apiCall("PATCH", `/workspaces/${params.workspace_id}/config`, params.config);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerFileTools(srv: McpServer) {
|
export function registerFileTools(srv: McpServer) {
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"list_files",
|
"list_files",
|
||||||
@ -89,7 +142,7 @@ export function registerFileTools(srv: McpServer) {
|
|||||||
"replace_all_files",
|
"replace_all_files",
|
||||||
"Replace all workspace config files at once",
|
"Replace all workspace config files at once",
|
||||||
{
|
{
|
||||||
workspace_id: z.string(),
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
files: z.record(z.string()).describe("Map of file path → content"),
|
files: z.record(z.string()).describe("Map of file path → content"),
|
||||||
},
|
},
|
||||||
handleReplaceAllFiles
|
handleReplaceAllFiles
|
||||||
@ -98,14 +151,14 @@ export function registerFileTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"get_config",
|
"get_config",
|
||||||
"Get workspace runtime config as JSON",
|
"Get workspace runtime config as JSON",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleGetConfig
|
handleGetConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"update_config",
|
"update_config",
|
||||||
"Update workspace runtime config",
|
"Update workspace runtime config",
|
||||||
{ workspace_id: z.string(), config: z.record(z.unknown()).describe("Config fields to update") },
|
{ workspace_id: z.string().describe("Workspace ID"), config: z.record(z.unknown()).describe("Config fields to update") },
|
||||||
handleUpdateConfig
|
handleUpdateConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,92 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
|
||||||
export async function handleListPluginRegistry() {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schemas
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ListInstalledPluginsSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ListInstalledPluginsParams = z.infer<typeof ListInstalledPluginsSchema>;
|
||||||
|
|
||||||
|
const InstallPluginSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
source: z.string().describe(
|
||||||
|
"Source URL: 'local://<name>' for platform registry, 'github://<owner>/<repo>[#<ref>]' for GitHub, or any registered scheme."
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export type InstallPluginParams = z.infer<typeof InstallPluginSchema>;
|
||||||
|
|
||||||
|
const UninstallPluginSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
name: z.string().describe("Plugin name to remove"),
|
||||||
|
});
|
||||||
|
export type UninstallPluginParams = z.infer<typeof UninstallPluginSchema>;
|
||||||
|
|
||||||
|
const ListAvailablePluginsSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ListAvailablePluginsParams = z.infer<typeof ListAvailablePluginsSchema>;
|
||||||
|
|
||||||
|
const CheckPluginCompatibilitySchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
runtime: z.string().describe("Target runtime (claude-code, deepagents, langgraph, ...)"),
|
||||||
|
});
|
||||||
|
export type CheckPluginCompatibilityParams = z.infer<typeof CheckPluginCompatibilitySchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleListPluginRegistry(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/plugins");
|
const data = await platformGet("/plugins");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListInstalledPlugins(params: { workspace_id: string }) {
|
export async function handleListInstalledPlugins(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListInstalledPluginsSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/plugins`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/plugins`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleInstallPlugin(params: { workspace_id: string; source: string }) {
|
export async function handleInstallPlugin(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, source } = params;
|
const params = validate(args, InstallPluginSchema);
|
||||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/plugins`, { source });
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/plugins`, { source: params.source });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleUninstallPlugin(params: { workspace_id: string; name: string }) {
|
export async function handleUninstallPlugin(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, name } = params;
|
const params = validate(args, UninstallPluginSchema);
|
||||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/plugins/${name}`);
|
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/plugins/${params.name}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListPluginSources() {
|
export async function handleListPluginSources(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/plugins/sources");
|
const data = await platformGet("/plugins/sources");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListAvailablePlugins(params: { workspace_id: string }) {
|
export async function handleListAvailablePlugins(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListAvailablePluginsSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/plugins/available`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/plugins/available`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleCheckPluginCompatibility(params: {
|
export async function handleCheckPluginCompatibility(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
workspace_id: string;
|
const params = validate(args, CheckPluginCompatibilitySchema);
|
||||||
runtime: string;
|
const data = await platformGet(
|
||||||
}) {
|
`/workspaces/${params.workspace_id}/plugins/compatibility?runtime=${encodeURIComponent(params.runtime)}`,
|
||||||
const { workspace_id, runtime } = params;
|
|
||||||
const data = await platformGet(`/workspaces/${workspace_id}/plugins/compatibility?runtime=${encodeURIComponent(runtime)}`,
|
|
||||||
);
|
);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerPluginTools(srv: McpServer) {
|
export function registerPluginTools(srv: McpServer) {
|
||||||
srv.tool("list_plugin_registry", "List all available plugins from the registry", {}, handleListPluginRegistry);
|
srv.tool("list_plugin_registry", "List all available plugins from the registry", {}, handleListPluginRegistry);
|
||||||
|
|
||||||
@ -59,11 +102,9 @@ export function registerPluginTools(srv: McpServer) {
|
|||||||
"Install a plugin into a workspace from any registered source (auto-restarts). Use GET /plugins/sources to list schemes.",
|
"Install a plugin into a workspace from any registered source (auto-restarts). Use GET /plugins/sources to list schemes.",
|
||||||
{
|
{
|
||||||
workspace_id: z.string().describe("Workspace ID"),
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
source: z
|
source: z.string().describe(
|
||||||
.string()
|
"Source URL: 'local://<name>' for platform registry, 'github://<owner>/<repo>[#<ref>]' for GitHub, or any registered scheme."
|
||||||
.describe(
|
),
|
||||||
"Source URL: 'local://<name>' for platform registry, 'github://<owner>/<repo>[#<ref>]' for GitHub, or any registered scheme."
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
handleInstallPlugin
|
handleInstallPlugin
|
||||||
);
|
);
|
||||||
@ -88,7 +129,7 @@ export function registerPluginTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"list_available_plugins",
|
"list_available_plugins",
|
||||||
"List plugins from the registry filtered to ones supported by this workspace's runtime.",
|
"List plugins from the registry filtered to ones supported by this workspace's runtime.",
|
||||||
{ workspace_id: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID") },
|
||||||
handleListAvailablePlugins,
|
handleListAvailablePlugins,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -96,7 +137,7 @@ export function registerPluginTools(srv: McpServer) {
|
|||||||
"check_plugin_compatibility",
|
"check_plugin_compatibility",
|
||||||
"Preflight check: which installed plugins would break if this workspace switched runtime to <runtime>?",
|
"Preflight check: which installed plugins would break if this workspace switched runtime to <runtime>?",
|
||||||
{
|
{
|
||||||
workspace_id: z.string(),
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
runtime: z.string().describe("Target runtime (claude-code, deepagents, langgraph, ...)"),
|
runtime: z.string().describe("Target runtime (claude-code, deepagents, langgraph, ...)"),
|
||||||
},
|
},
|
||||||
handleCheckPluginCompatibility,
|
handleCheckPluginCompatibility,
|
||||||
|
|||||||
@ -1,49 +1,89 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
import { apiCall, platformGet, toMcpResult } from "../api.js";
|
||||||
|
import { validate } from "../utils/validation.js";
|
||||||
|
|
||||||
export async function handleSetSecret(params: { workspace_id: string; key: string; value: string }) {
|
// ---------------------------------------------------------------------------
|
||||||
const { workspace_id, key, value } = params;
|
// Schemas
|
||||||
const data = await apiCall("POST", `/workspaces/${workspace_id}/secrets`, { key, value });
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SetSecretSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
key: z.string().describe("Secret key (e.g., ANTHROPIC_API_KEY)"),
|
||||||
|
value: z.string().describe("Secret value"),
|
||||||
|
});
|
||||||
|
export type SetSecretParams = z.infer<typeof SetSecretSchema>;
|
||||||
|
|
||||||
|
const ListSecretsSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
});
|
||||||
|
export type ListSecretsParams = z.infer<typeof ListSecretsSchema>;
|
||||||
|
|
||||||
|
const DeleteSecretSchema = z.object({
|
||||||
|
workspace_id: z.string().describe("Workspace ID"),
|
||||||
|
key: z.string().describe("Secret key"),
|
||||||
|
});
|
||||||
|
export type DeleteSecretParams = z.infer<typeof DeleteSecretSchema>;
|
||||||
|
|
||||||
|
const SetGlobalSecretSchema = z.object({
|
||||||
|
key: z.string().describe("Secret key (e.g., GITHUB_TOKEN)"),
|
||||||
|
value: z.string().describe("Secret value"),
|
||||||
|
});
|
||||||
|
export type SetGlobalSecretParams = z.infer<typeof SetGlobalSecretSchema>;
|
||||||
|
|
||||||
|
const DeleteGlobalSecretSchema = z.object({
|
||||||
|
key: z.string().describe("Secret key"),
|
||||||
|
});
|
||||||
|
export type DeleteGlobalSecretParams = z.infer<typeof DeleteGlobalSecretSchema>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleSetSecret(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, SetSecretSchema);
|
||||||
|
const data = await apiCall("POST", `/workspaces/${params.workspace_id}/secrets`, { key: params.key, value: params.value });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListSecrets(params: { workspace_id: string }) {
|
export async function handleListSecrets(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, ListSecretsSchema);
|
||||||
const data = await platformGet(`/workspaces/${params.workspace_id}/secrets`);
|
const data = await platformGet(`/workspaces/${params.workspace_id}/secrets`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDeleteSecret(params: { workspace_id: string; key: string }) {
|
export async function handleDeleteSecret(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { workspace_id, key } = params;
|
const params = validate(args, DeleteSecretSchema);
|
||||||
const data = await apiCall("DELETE", `/workspaces/${workspace_id}/secrets/${encodeURIComponent(key)}`);
|
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}/secrets/${encodeURIComponent(params.key)}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleListGlobalSecrets() {
|
export async function handleListGlobalSecrets(): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const data = await platformGet("/settings/secrets");
|
const data = await platformGet("/settings/secrets");
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSetGlobalSecret(params: { key: string; value: string }) {
|
export async function handleSetGlobalSecret(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
const { key, value } = params;
|
const params = validate(args, SetGlobalSecretSchema);
|
||||||
const data = await apiCall("PUT", "/settings/secrets", { key, value });
|
const data = await apiCall("PUT", "/settings/secrets", { key: params.key, value: params.value });
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDeleteGlobalSecret(params: { key: string }) {
|
export async function handleDeleteGlobalSecret(args: unknown): Promise<ReturnType<typeof toMcpResult>> {
|
||||||
|
const params = validate(args, DeleteGlobalSecretSchema);
|
||||||
const data = await apiCall("DELETE", `/settings/secrets/${params.key}`);
|
const data = await apiCall("DELETE", `/settings/secrets/${params.key}`);
|
||||||
return toMcpResult(data);
|
return toMcpResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function registerSecretTools(srv: McpServer) {
|
export function registerSecretTools(srv: McpServer) {
|
||||||
srv.tool(
|
srv.tool(
|
||||||
"set_secret",
|
"set_secret",
|
||||||
"Set an API key or environment variable for a workspace",
|
"Set an API key or environment variable for a workspace",
|
||||||
{
|
{ workspace_id: z.string().describe("Workspace ID"), key: z.string().describe("Secret key (e.g., ANTHROPIC_API_KEY)"), value: z.string().describe("Secret value") },
|
||||||
workspace_id: z.string().describe("Workspace ID"),
|
|
||||||
key: z.string().describe("Secret key (e.g., ANTHROPIC_API_KEY)"),
|
|
||||||
value: z.string().describe("Secret value"),
|
|
||||||
},
|
|
||||||
handleSetSecret
|
handleSetSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -57,7 +97,7 @@ export function registerSecretTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"delete_secret",
|
"delete_secret",
|
||||||
"Delete a secret from a workspace",
|
"Delete a secret from a workspace",
|
||||||
{ workspace_id: z.string(), key: z.string() },
|
{ workspace_id: z.string().describe("Workspace ID"), key: z.string().describe("Secret key") },
|
||||||
handleDeleteSecret
|
handleDeleteSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,10 +106,7 @@ export function registerSecretTools(srv: McpServer) {
|
|||||||
srv.tool(
|
srv.tool(
|
||||||
"set_global_secret",
|
"set_global_secret",
|
||||||
"Set a global secret (available to all workspaces)",
|
"Set a global secret (available to all workspaces)",
|
||||||
{
|
{ key: z.string().describe("Secret key (e.g., GITHUB_TOKEN)"), value: z.string().describe("Secret value") },
|
||||||
key: z.string().describe("Secret key (e.g., GITHUB_TOKEN)"),
|
|
||||||
value: z.string().describe("Secret value"),
|
|
||||||
},
|
|
||||||
handleSetGlobalSecret
|
handleSetGlobalSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
115
src/utils/validation.ts
Normal file
115
src/utils/validation.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Shared input validation utilities for MCP tool handlers.
|
||||||
|
*
|
||||||
|
* MCP tool arguments arrive as raw JSON (unknown). Before passing them to any
|
||||||
|
* business logic, every handler validates them against its Zod schema.
|
||||||
|
* On parse failure the handler returns a structured INVALID_ARGUMENTS error
|
||||||
|
* (MCP error code -32602) rather than letting type/structure errors surface
|
||||||
|
* as INTERNAL_ERROR later in the call stack.
|
||||||
|
*
|
||||||
|
* This also serves as living documentation: each schema documents exactly what
|
||||||
|
* a tool accepts, what types are required/optional, and what constraints apply.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ZodError, ZodSchema, z } from "zod";
|
||||||
|
|
||||||
|
/** MCP JSON-RPC error codes used by this server. */
|
||||||
|
export const ErrorCode = {
|
||||||
|
InvalidParams: -32602,
|
||||||
|
InternalError: -32603,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// INVALID_ARGUMENTS error
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured MCP error for INVALID_ARGUMENTS.
|
||||||
|
*
|
||||||
|
* MCP error response shape:
|
||||||
|
* { content: [{ type: "text", text: "<formatted message>" }],
|
||||||
|
* isError: true }
|
||||||
|
*
|
||||||
|
* The MCP SDK translates a handler that throws `new InvalidArgumentsError(...)`
|
||||||
|
* into an INVALID_ARGUMENTS response (JSON-RPC error code -32602).
|
||||||
|
* If a handler returns normally the SDK returns isError: false.
|
||||||
|
*/
|
||||||
|
export class InvalidArgumentsError extends Error {
|
||||||
|
/** Zod validation issues, one per line, human-readable. */
|
||||||
|
readonly issues: string[];
|
||||||
|
|
||||||
|
constructor(issues: string[]) {
|
||||||
|
super(formatIssues(issues));
|
||||||
|
this.name = "InvalidArgumentsError";
|
||||||
|
this.issues = issues;
|
||||||
|
// Make the error look like an MCP SDK error for the framework.
|
||||||
|
Object.setPrototypeOf(this, InvalidArgumentsError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a list of Zod issues into a single readable string. */
|
||||||
|
function formatIssues(issues: string[]): string {
|
||||||
|
if (issues.length === 1) return `Invalid argument: ${issues[0]}`;
|
||||||
|
return `Invalid arguments (${issues.length} errors):\n${issues.map((e) => ` - ${e}`).join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a Zod ZodError into a flat list of human-readable issue strings.
|
||||||
|
* Each entry is "[field]: [message]" or just "[message]" for root issues.
|
||||||
|
*/
|
||||||
|
export function formatZodIssues(err: ZodError): string[] {
|
||||||
|
return err.issues.map((issue) => {
|
||||||
|
const path = issue.path.length > 0 ? issue.path.join(".") + ": " : "";
|
||||||
|
return path + issue.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Core validate helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate `args` against `schema` and return the parsed value on success.
|
||||||
|
*
|
||||||
|
* Usage — add ONE line at the top of every handler:
|
||||||
|
* const params = validate(args, MyToolSchema);
|
||||||
|
*
|
||||||
|
* Throws `InvalidArgumentsError` (caught by the MCP SDK → INVALID_ARGUMENTS
|
||||||
|
* response) if validation fails. The error message lists every failure.
|
||||||
|
*
|
||||||
|
* @param args - Raw JSON object received from the MCP caller.
|
||||||
|
* @param schema - Zod schema (sync or async) that describes the expected shape.
|
||||||
|
* @returns The parsed and typed arguments.
|
||||||
|
* @throws InvalidArgumentsError if args fail validation.
|
||||||
|
*/
|
||||||
|
export function validate<T>(args: unknown, schema: ZodSchema<T>): T {
|
||||||
|
if (args == null) args = {};
|
||||||
|
|
||||||
|
const result = schema.safeParse(args);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new InvalidArgumentsError(formatZodIssues(result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Optional-param guard
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw INVALID_ARGUMENTS if `value` is null or undefined.
|
||||||
|
* Use for required params that Zod's `.required()` alone cannot catch when
|
||||||
|
* the caller sends `null` instead of omitting the key.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* const { workspace_id } = validate(args, SomeSchema);
|
||||||
|
* guardRequired(workspace_id, "workspace_id");
|
||||||
|
*/
|
||||||
|
export function guardRequired<T>(value: T, fieldName: string): T {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
throw new InvalidArgumentsError([`${fieldName}: required`]);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user