snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
58
plugins-external/mcp-server/README.md
Normal file
58
plugins-external/mcp-server/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Yaak MCP Server Plugin
|
||||
|
||||
Exposes Yaak's functionality via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
|
||||
|
||||
## Setup
|
||||
|
||||
Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"yaak": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://127.0.0.1:64343/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Desktop and make sure Yaak is running.
|
||||
|
||||
## Available Tools
|
||||
|
||||
- `list_http_requests` - List all HTTP requests in a workspace
|
||||
- `get_http_request` - Get details of a specific HTTP request
|
||||
- `send_http_request` - Send an HTTP request and get the response
|
||||
- `create_http_request` - Create a new HTTP request
|
||||
- `update_http_request` - Update an existing HTTP request
|
||||
- `delete_http_request` - Delete an HTTP request
|
||||
- `list_folders` - List all folders in a workspace
|
||||
- `list_workspaces` - List all open workspaces
|
||||
- `get_workspace_id` - Get the current workspace ID
|
||||
- `get_environment_id` - Get the current environment ID
|
||||
- `copy_to_clipboard` - Copy text to the system clipboard
|
||||
- `show_toast` - Show a toast notification in Yaak
|
||||
|
||||
## Environment Selection
|
||||
|
||||
`send_http_request` accepts an optional `environmentId` for that execution. It overrides the
|
||||
active environment without changing the selection in Yaak. Omitting it uses the host's
|
||||
active environment, as before.
|
||||
|
||||
The ID must identify a base or sub-environment in the request's workspace. An empty,
|
||||
unknown, foreign-workspace, or folder environment ID returns an error before sending.
|
||||
Use the base environment ID to run with global variables only. Folder variables still
|
||||
apply according to the request's folder hierarchy.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "rq_example",
|
||||
"workspaceId": "wk_example",
|
||||
"environmentId": "ev_staging"
|
||||
}
|
||||
```
|
||||
|
||||
This requires a Yaak host with support for the `environmentId` plugin API field;
|
||||
older hosts ignore it. `get_environment_id` reports the active selection, not the
|
||||
override used by a previous send.
|
||||
28
plugins-external/mcp-server/package.json
Normal file
28
plugins-external/mcp-server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@yaak/mcp-server",
|
||||
"displayName": "MCP Server",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"description": "Expose Yaak functionality via Model Context Protocol",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
"directory": "plugins-external/mcp-server"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/mcp": "^0.3.2",
|
||||
"@hono/node-server": "^2.0.10",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"hono": "^4.13.5",
|
||||
"zod": "^4.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.3",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"minYaakVersion": "2026.1.0"
|
||||
}
|
||||
36
plugins-external/mcp-server/src/index.ts
Normal file
36
plugins-external/mcp-server/src/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Context, PluginDefinition } from "@yaakapp/api";
|
||||
import { createMcpServer } from "./server.js";
|
||||
|
||||
const serverPort = parseInt(process.env.YAAK_PLUGIN_MCP_SERVER_PORT ?? "64343", 10);
|
||||
|
||||
let mcpServer: Awaited<ReturnType<typeof createMcpServer>> | null = null;
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
async init(ctx: Context) {
|
||||
// Start the server after waiting, so there's an active window open to do things
|
||||
// like show the startup toast.
|
||||
console.log("Initializing MCP Server plugin");
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
mcpServer = createMcpServer({ yaak: ctx }, serverPort);
|
||||
} catch (err) {
|
||||
console.error("Failed to start MCP server:", err);
|
||||
void ctx.toast.show({
|
||||
message: `Failed to start MCP Server: ${err instanceof Error ? err.message : String(err)}`,
|
||||
icon: "alert_triangle",
|
||||
color: "danger",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
async dispose() {
|
||||
console.log("Disposing MCP Server plugin");
|
||||
|
||||
if (mcpServer) {
|
||||
await mcpServer.close();
|
||||
mcpServer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
72
plugins-external/mcp-server/src/server.ts
Normal file
72
plugins-external/mcp-server/src/server.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { StreamableHTTPTransport } from "@hono/mcp";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { Hono } from "hono";
|
||||
import { registerFolderTools } from "./tools/folder.js";
|
||||
import { registerHttpRequestTools } from "./tools/httpRequest.js";
|
||||
import { registerToastTools } from "./tools/toast.js";
|
||||
import { registerWindowTools } from "./tools/window.js";
|
||||
import { registerWorkspaceTools } from "./tools/workspace.js";
|
||||
import type { McpServerContext } from "./types.js";
|
||||
|
||||
export function createMcpServer(ctx: McpServerContext, port: number) {
|
||||
console.log("Creating MCP server on port", port);
|
||||
const mcpServer = new McpServer({
|
||||
name: "yaak-mcp-server",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
// Register all tools
|
||||
registerToastTools(mcpServer, ctx);
|
||||
registerHttpRequestTools(mcpServer, ctx);
|
||||
registerFolderTools(mcpServer, ctx);
|
||||
registerWindowTools(mcpServer, ctx);
|
||||
registerWorkspaceTools(mcpServer, ctx);
|
||||
|
||||
const app = new Hono();
|
||||
const transport = new StreamableHTTPTransport();
|
||||
|
||||
app.all("/mcp", async (c) => {
|
||||
if (!mcpServer.isConnected()) {
|
||||
// Connect the mcp with the transport
|
||||
await mcpServer.connect(transport);
|
||||
void ctx.yaak.toast.show({
|
||||
message: `MCP Server connected`,
|
||||
icon: "info",
|
||||
color: "info",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
return transport.handleRequest(c);
|
||||
});
|
||||
|
||||
const honoServer = serve(
|
||||
{
|
||||
port,
|
||||
hostname: "127.0.0.1",
|
||||
fetch: app.fetch,
|
||||
},
|
||||
(info) => {
|
||||
console.log("Started MCP server on ", info.address);
|
||||
void ctx.yaak.toast.show({
|
||||
message: `MCP Server running on http://127.0.0.1:${info.port}`,
|
||||
icon: "info",
|
||||
color: "secondary",
|
||||
timeout: 10000,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
server: mcpServer,
|
||||
close: async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
honoServer.close((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
await mcpServer.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
148
plugins-external/mcp-server/src/tools/folder.ts
Normal file
148
plugins-external/mcp-server/src/tools/folder.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import * as z from "zod";
|
||||
import type { McpServerContext } from "../types.js";
|
||||
import { getWorkspaceContext } from "./helpers.js";
|
||||
import {
|
||||
authenticationSchema,
|
||||
authenticationTypeSchema,
|
||||
headersSchema,
|
||||
workspaceIdSchema,
|
||||
} from "./schemas.js";
|
||||
|
||||
export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
|
||||
server.registerTool(
|
||||
"list_folders",
|
||||
{
|
||||
title: "List Folders",
|
||||
description: "List all folders in a workspace",
|
||||
inputSchema: {
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ workspaceId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const folders = await workspaceCtx.yaak.folder.list();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(folders, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_folder",
|
||||
{
|
||||
title: "Get Folder",
|
||||
description: "Get details of a specific folder by ID",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The folder ID"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const folder = await workspaceCtx.yaak.folder.getById({ id });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(folder, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"create_folder",
|
||||
{
|
||||
title: "Create Folder",
|
||||
description: "Create a new folder in a workspace",
|
||||
inputSchema: {
|
||||
workspaceId: workspaceIdSchema,
|
||||
name: z.string().describe("Folder name"),
|
||||
folderId: z.string().optional().describe("Parent folder ID (for nested folders)"),
|
||||
description: z.string().optional().describe("Folder description"),
|
||||
sortPriority: z.number().optional().describe("Sort priority for ordering"),
|
||||
headers: headersSchema.describe("Default headers to apply to requests in this folder"),
|
||||
authenticationType: authenticationTypeSchema,
|
||||
authentication: authenticationSchema,
|
||||
},
|
||||
},
|
||||
async ({ workspaceId: ogWorkspaceId, ...args }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, ogWorkspaceId);
|
||||
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
|
||||
if (!workspaceId) {
|
||||
throw new Error("No workspace is open");
|
||||
}
|
||||
|
||||
const folder = await workspaceCtx.yaak.folder.create({
|
||||
workspaceId: workspaceId,
|
||||
...args,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(folder, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"update_folder",
|
||||
{
|
||||
title: "Update Folder",
|
||||
description: "Update an existing folder",
|
||||
inputSchema: {
|
||||
id: z.string().describe("Folder ID to update"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
name: z.string().optional().describe("Folder name"),
|
||||
folderId: z.string().optional().describe("Parent folder ID (for nested folders)"),
|
||||
description: z.string().optional().describe("Folder description"),
|
||||
sortPriority: z.number().optional().describe("Sort priority for ordering"),
|
||||
headers: headersSchema.describe("Default headers to apply to requests in this folder"),
|
||||
authenticationType: authenticationTypeSchema,
|
||||
authentication: authenticationSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId, ...updates }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
// Fetch existing folder to merge with updates
|
||||
const existing = await workspaceCtx.yaak.folder.getById({ id });
|
||||
if (!existing) {
|
||||
throw new Error(`Folder with ID ${id} not found`);
|
||||
}
|
||||
// Merge existing fields with updates
|
||||
const folder = await workspaceCtx.yaak.folder.update({
|
||||
...existing,
|
||||
...updates,
|
||||
id,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(folder, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"delete_folder",
|
||||
{
|
||||
title: "Delete Folder",
|
||||
description: "Delete a folder by ID",
|
||||
inputSchema: {
|
||||
id: z.string().describe("Folder ID to delete"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
const folder = await ctx.yaak.folder.delete({ id });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Deleted: ${folder.name} (${folder.id})` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
32
plugins-external/mcp-server/src/tools/helpers.ts
Normal file
32
plugins-external/mcp-server/src/tools/helpers.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { McpServerContext } from "../types.js";
|
||||
|
||||
export async function getWorkspaceContext(
|
||||
ctx: McpServerContext,
|
||||
workspaceId?: string,
|
||||
): Promise<McpServerContext> {
|
||||
const workspaces = await ctx.yaak.workspace.list();
|
||||
|
||||
if (!workspaceId && workspaces.length > 1) {
|
||||
const workspaceList = workspaces.map((w, i) => `${i + 1}. ${w.name} (ID: ${w.id})`).join("\n");
|
||||
throw new Error(
|
||||
`Multiple workspaces are open. Please specify which workspace to use.\n\n` +
|
||||
`Currently open workspaces:\n${workspaceList}\n\n` +
|
||||
`You can use the list_workspaces tool to get workspace IDs, then use other tools ` +
|
||||
`with the workspace context. For example, ask the user which workspace they want ` +
|
||||
`to work with by presenting them with the numbered list above.`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = workspaceId ? workspaces.find((w) => w.id === workspaceId) : workspaces[0];
|
||||
if (!workspace) {
|
||||
const workspaceList = workspaces.map((w) => `- ${w.name} (ID: ${w.id})`).join("\n");
|
||||
throw new Error(
|
||||
`Workspace with ID "${workspaceId}" not found.\n\n` +
|
||||
`Available workspaces:\n${workspaceList}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
yaak: ctx.yaak.workspace.withContext(workspace),
|
||||
};
|
||||
}
|
||||
98
plugins-external/mcp-server/src/tools/httpRequest.test.ts
Normal file
98
plugins-external/mcp-server/src/tools/httpRequest.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { afterEach, expect, test, vi } from "vite-plus/test";
|
||||
import { registerHttpRequestTools } from "./httpRequest";
|
||||
|
||||
const cleanups: Array<() => Promise<void>> = [];
|
||||
afterEach(async () => {
|
||||
for (const cleanup of cleanups.splice(0)) await cleanup();
|
||||
});
|
||||
|
||||
async function fixture({ missing = false, sendError = false } = {}) {
|
||||
const httpRequest = { id: "rq_test", workspaceId: "wk_test", url: "http://localhost/echo" };
|
||||
const send = vi.fn(async (_args: { httpRequest: unknown; environmentId?: string }) => {
|
||||
if (sendError) throw new Error("Environment not found");
|
||||
return { httpResponse: { id: "rs_test", status: 200 } };
|
||||
});
|
||||
// Only the host is mocked; calls go through the real MCP client, schema and transport.
|
||||
const yaak = {
|
||||
workspace: {
|
||||
list: async () => [{ id: "wk_test", name: "Test Workspace" }],
|
||||
withContext: () => yaak,
|
||||
},
|
||||
httpRequest: { getById: async () => (missing ? null : httpRequest), send },
|
||||
};
|
||||
const server = new McpServer({ name: "yaak-test", version: "0.0.0" });
|
||||
registerHttpRequestTools(server, { yaak: yaak as unknown as Context });
|
||||
const client = new Client({ name: "test", version: "0.0.0" });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
cleanups.push(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
});
|
||||
return { client, send, httpRequest };
|
||||
}
|
||||
|
||||
test("forwards the advertised environmentId to the host", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
const { tools } = await client.listTools();
|
||||
expect(
|
||||
tools.find((tool) => tool.name === "send_http_request")?.inputSchema.properties,
|
||||
).toHaveProperty("environmentId");
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id, workspaceId: "wk_test", environmentId: "ev_staging" },
|
||||
});
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(send).toHaveBeenCalledExactlyOnceWith({ httpRequest, environmentId: "ev_staging" });
|
||||
});
|
||||
|
||||
test("leaves the environment unspecified when omitted", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id },
|
||||
});
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(send).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ httpRequest }));
|
||||
expect(send.mock.calls[0]?.[0]).not.toHaveProperty("environmentId", expect.any(String));
|
||||
});
|
||||
|
||||
test("does not send a missing request", async () => {
|
||||
const { client, send } = await fixture({ missing: true });
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: "rq_missing" },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns host environment errors to the MCP client", async () => {
|
||||
const { client } = await fixture({ sendError: true });
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: "rq_test", environmentId: "ev_missing" },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toEqual([{ type: "text", text: "Environment not found" }]);
|
||||
});
|
||||
|
||||
test("keeps concurrent environment overrides separate", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
await Promise.all(
|
||||
["ev_a", "ev_b"].map((environmentId) =>
|
||||
client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id, environmentId },
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(send).toHaveBeenCalledTimes(2);
|
||||
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_a" });
|
||||
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_b" });
|
||||
});
|
||||
201
plugins-external/mcp-server/src/tools/httpRequest.ts
Normal file
201
plugins-external/mcp-server/src/tools/httpRequest.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import * as z from "zod";
|
||||
import type { McpServerContext } from "../types.js";
|
||||
import { getWorkspaceContext } from "./helpers.js";
|
||||
import {
|
||||
authenticationSchema,
|
||||
authenticationTypeSchema,
|
||||
bodySchema,
|
||||
bodyTypeSchema,
|
||||
headersSchema,
|
||||
urlParametersSchema,
|
||||
workspaceIdSchema,
|
||||
} from "./schemas.js";
|
||||
|
||||
export function registerHttpRequestTools(server: McpServer, ctx: McpServerContext) {
|
||||
server.registerTool(
|
||||
"list_http_requests",
|
||||
{
|
||||
title: "List HTTP Requests",
|
||||
description: "List all HTTP requests in a workspace",
|
||||
inputSchema: {
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ workspaceId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const requests = await workspaceCtx.yaak.httpRequest.list();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(requests, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_http_request",
|
||||
{
|
||||
title: "Get HTTP Request",
|
||||
description: "Get details of a specific HTTP request by ID",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The HTTP request ID"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const request = await workspaceCtx.yaak.httpRequest.getById({ id });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(request, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"send_http_request",
|
||||
{
|
||||
title: "Send HTTP Request",
|
||||
description: "Send an HTTP request and get the response",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The HTTP request ID to send"),
|
||||
environmentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Environment ID for this send only; defaults to the active environment"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId, environmentId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const httpRequest = await workspaceCtx.yaak.httpRequest.getById({ id });
|
||||
if (httpRequest == null) {
|
||||
throw new Error(`HTTP request with ID ${id} not found`);
|
||||
}
|
||||
|
||||
const { httpResponse: response } = await workspaceCtx.yaak.httpRequest.send({
|
||||
httpRequest,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"create_http_request",
|
||||
{
|
||||
title: "Create HTTP Request",
|
||||
description: "Create a new HTTP request",
|
||||
inputSchema: {
|
||||
workspaceId: workspaceIdSchema,
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Request name (empty string to auto-generate from URL)"),
|
||||
url: z.string().describe("Request URL"),
|
||||
method: z.string().optional().describe("HTTP method (defaults to GET)"),
|
||||
folderId: z.string().optional().describe("Parent folder ID"),
|
||||
description: z.string().optional().describe("Request description"),
|
||||
headers: headersSchema.describe("Request headers"),
|
||||
urlParameters: urlParametersSchema,
|
||||
bodyType: bodyTypeSchema,
|
||||
body: bodySchema,
|
||||
authenticationType: authenticationTypeSchema,
|
||||
authentication: authenticationSchema,
|
||||
},
|
||||
},
|
||||
async ({ workspaceId: ogWorkspaceId, ...args }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, ogWorkspaceId);
|
||||
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
|
||||
if (!workspaceId) {
|
||||
throw new Error("No workspace is open");
|
||||
}
|
||||
|
||||
const httpRequest = await workspaceCtx.yaak.httpRequest.create({
|
||||
workspaceId: workspaceId,
|
||||
...args,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(httpRequest, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"update_http_request",
|
||||
{
|
||||
title: "Update HTTP Request",
|
||||
description: "Update an existing HTTP request",
|
||||
inputSchema: {
|
||||
id: z.string().describe("HTTP request ID to update"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
name: z.string().optional().describe("Request name"),
|
||||
url: z.string().optional().describe("Request URL"),
|
||||
method: z.string().optional().describe("HTTP method"),
|
||||
folderId: z.string().optional().describe("Parent folder ID"),
|
||||
description: z.string().optional().describe("Request description"),
|
||||
headers: headersSchema.describe("Request headers"),
|
||||
urlParameters: urlParametersSchema,
|
||||
bodyType: bodyTypeSchema,
|
||||
body: bodySchema,
|
||||
authenticationType: authenticationTypeSchema,
|
||||
authentication: authenticationSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId, ...updates }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
// Fetch existing request to merge with updates
|
||||
const existing = await workspaceCtx.yaak.httpRequest.getById({ id });
|
||||
if (!existing) {
|
||||
throw new Error(`HTTP request with ID ${id} not found`);
|
||||
}
|
||||
// Merge existing fields with updates
|
||||
const httpRequest = await workspaceCtx.yaak.httpRequest.update({
|
||||
...existing,
|
||||
...updates,
|
||||
id,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(httpRequest, null, 2) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"delete_http_request",
|
||||
{
|
||||
title: "Delete HTTP Request",
|
||||
description: "Delete an HTTP request by ID",
|
||||
inputSchema: {
|
||||
id: z.string().describe("HTTP request ID to delete"),
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
const httpRequest = await ctx.yaak.httpRequest.delete({ id });
|
||||
return {
|
||||
content: [
|
||||
{ type: "text" as const, text: `Deleted: ${httpRequest.name} (${httpRequest.id})` },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal file
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as z from "zod";
|
||||
|
||||
export const workspaceIdSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Workspace ID (required if multiple workspaces are open)");
|
||||
|
||||
export const headersSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
enabled: z.boolean().default(true),
|
||||
}),
|
||||
)
|
||||
.optional();
|
||||
|
||||
export const urlParametersSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
enabled: z.boolean().default(true),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe("URL query parameters");
|
||||
|
||||
export const bodyTypeSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Body type. Supported values: "binary", "graphql", "application/x-www-form-urlencoded", "multipart/form-data", or any text-based type (e.g., "application/json", "text/plain")',
|
||||
);
|
||||
|
||||
export const bodySchema = z
|
||||
.record(z.string(), z.any())
|
||||
.optional()
|
||||
.describe(
|
||||
"Body content object. Structure varies by bodyType:\n" +
|
||||
'- "binary": { filePath: "/path/to/file" }\n' +
|
||||
'- "graphql": { query: "{ users { name } }", variables: "{\\"id\\": \\"123\\"}" }\n' +
|
||||
'- "application/x-www-form-urlencoded": { form: [{ name: "key", value: "val", enabled: true }] }\n' +
|
||||
'- "multipart/form-data": { form: [{ name: "field", value: "text", file: "/path/to/file", enabled: true }] }\n' +
|
||||
'- text-based (application/json, etc.): { text: "raw body content" }',
|
||||
);
|
||||
|
||||
export const authenticationTypeSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent.',
|
||||
);
|
||||
|
||||
export const authenticationSchema = z
|
||||
.record(z.string(), z.any())
|
||||
.optional()
|
||||
.describe(
|
||||
"Authentication configuration object. Structure varies by authenticationType:\n" +
|
||||
'- "basic": { username: "user", password: "pass" }\n' +
|
||||
'- "bearer": { token: "abc123", prefix: "Bearer" }\n' +
|
||||
'- "oauth2": { clientId: "...", clientSecret: "...", grantType: "authorization_code", authorizationUrl: "...", accessTokenUrl: "...", scope: "...", ... }\n' +
|
||||
'- "apikey": { location: "header" | "query", key: "X-API-Key", value: "..." }\n' +
|
||||
'- "jwt": { algorithm: "HS256", secret: "...", payload: "{ ... }" }\n' +
|
||||
'- "awsv4": { accessKeyId: "...", secretAccessKey: "...", service: "sts", region: "us-east-1", sessionToken: "..." }\n' +
|
||||
'- "none": {}',
|
||||
);
|
||||
59
plugins-external/mcp-server/src/tools/toast.ts
Normal file
59
plugins-external/mcp-server/src/tools/toast.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { Color, Icon } from "@yaakapp/api";
|
||||
import * as z from "zod";
|
||||
import type { McpServerContext } from "../types.js";
|
||||
|
||||
const ICON_VALUES = [
|
||||
"alert_triangle",
|
||||
"check",
|
||||
"check_circle",
|
||||
"chevron_down",
|
||||
"copy",
|
||||
"info",
|
||||
"pin",
|
||||
"search",
|
||||
"trash",
|
||||
] as const satisfies readonly Icon[];
|
||||
|
||||
const COLOR_VALUES = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"info",
|
||||
"success",
|
||||
"notice",
|
||||
"warning",
|
||||
"danger",
|
||||
] as const satisfies readonly Color[];
|
||||
|
||||
export function registerToastTools(server: McpServer, ctx: McpServerContext) {
|
||||
server.registerTool(
|
||||
"show_toast",
|
||||
{
|
||||
title: "Show Toast",
|
||||
description: "Show a toast notification in Yaak",
|
||||
inputSchema: {
|
||||
message: z.string().describe("The message to display"),
|
||||
icon: z.enum(ICON_VALUES).optional().describe("Icon name"),
|
||||
color: z.enum(COLOR_VALUES).optional().describe("Toast color"),
|
||||
timeout: z.number().optional().describe("Timeout in milliseconds"),
|
||||
},
|
||||
},
|
||||
async ({ message, icon, color, timeout }) => {
|
||||
await ctx.yaak.toast.show({
|
||||
message,
|
||||
icon,
|
||||
color,
|
||||
timeout,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `✓ Toast shown: "${message}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
47
plugins-external/mcp-server/src/tools/window.ts
Normal file
47
plugins-external/mcp-server/src/tools/window.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { McpServerContext } from "../types.js";
|
||||
import { getWorkspaceContext } from "./helpers.js";
|
||||
|
||||
export function registerWindowTools(server: McpServer, ctx: McpServerContext) {
|
||||
server.registerTool(
|
||||
"get_workspace_id",
|
||||
{
|
||||
title: "Get Workspace ID",
|
||||
description: "Get the current workspace ID",
|
||||
},
|
||||
async () => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx);
|
||||
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: workspaceId || "No workspace open",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_environment_id",
|
||||
{
|
||||
title: "Get Environment ID",
|
||||
description: "Get the current environment ID",
|
||||
},
|
||||
async () => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx);
|
||||
const environmentId = await workspaceCtx.yaak.window.environmentId();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: environmentId || "No environment selected",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
24
plugins-external/mcp-server/src/tools/workspace.ts
Normal file
24
plugins-external/mcp-server/src/tools/workspace.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { McpServerContext } from "../types.js";
|
||||
|
||||
export function registerWorkspaceTools(server: McpServer, ctx: McpServerContext) {
|
||||
server.registerTool(
|
||||
"list_workspaces",
|
||||
{
|
||||
title: "List Workspaces",
|
||||
description: "List all open workspaces in Yaak",
|
||||
},
|
||||
async () => {
|
||||
const workspaces = await ctx.yaak.workspace.list();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify(workspaces, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
5
plugins-external/mcp-server/src/types.ts
Normal file
5
plugins-external/mcp-server/src/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Context } from "@yaakapp/api";
|
||||
|
||||
export interface McpServerContext {
|
||||
yaak: Context;
|
||||
}
|
||||
7
plugins-external/mcp-server/tsconfig.json
Normal file
7
plugins-external/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user