MCP Server Plugin (#335)

This commit is contained in:
Gregory Schier
2025-12-31 08:41:57 -08:00
committed by GitHub
parent 58eff84f43
commit 6b9b207e1c
44 changed files with 2144 additions and 191 deletions

1
plugins-external/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*/build

View File

@@ -0,0 +1,76 @@
# Yaak Faker Plugin
This is a template function that generates realistic fake data
for testing and development using [FakerJS](https://fakerjs.dev).
![CleanShot 2024-09-19 at 13 56 33@2x](https://github.com/user-attachments/assets/2f935110-4af2-4236-a50d-18db5454176d)
## Example JSON Body
Here's an example JSON body that uses fake data:
```json
{
"id": "${[ faker.string.uuid() ]}",
"name": "${[ faker.person.fullName() ]}",
"email": "${[ faker.internet.email() ]}",
"phone": "${[ faker.phone.number() ]}",
"address": {
"street": "${[ faker.location.streetAddress() ]}",
"city": "${[ faker.location.city() ]}",
"country": "${[ faker.location.country() ]}",
"zipCode": "${[ faker.location.zipCode() ]}"
},
"company": "${[ faker.company.name() ]}",
"website": "${[ faker.internet.url() ]}"
}
```
This will generate a random JSON body on every request:
```json
{
"id": "589f0aec-7310-4bf2-81c4-0b1bb7f1c3c1",
"name": "Lucy Gottlieb-Weissnat",
"email": "Destiny_Herzog@gmail.com",
"phone": "411.805.2871 x699",
"address": {
"street": "846 Christ Mills",
"city": "Spencerfurt",
"country": "United Kingdom",
"zipCode": "20354"
},
"company": "Emard, Kohler and Rutherford",
"website": "https://watery-detective.org"
}
```
## Available Categories
The plugin provides access to all FakerJS modules and their methods:
| Category | Description | Example Methods |
|------------|---------------------------|--------------------------------------------|
| `airline` | Airline-related data | `aircraftType`, `airline`, `airplane` |
| `animal` | Animal names and types | `bear`, `bird`, `cat`, `dog`, `fish` |
| `color` | Colors in various formats | `human`, `rgb`, `hex`, `hsl` |
| `commerce` | E-commerce data | `department`, `product`, `price` |
| `company` | Company information | `name`, `catchPhrase`, `bs` |
| `database` | Database-related data | `column`, `type`, `collation` |
| `date` | Date and time values | `recent`, `future`, `past`, `between` |
| `finance` | Financial data | `account`, `amount`, `currency` |
| `git` | Git-related data | `branch`, `commitEntry`, `commitSha` |
| `hacker` | Tech/hacker terminology | `abbreviation`, `noun`, `phrase` |
| `image` | Image URLs and data | `avatar`, `url`, `dataUri` |
| `internet` | Internet-related data | `email`, `url`, `ip`, `userAgent` |
| `location` | Geographic data | `city`, `country`, `latitude`, `longitude` |
| `lorem` | Lorem ipsum text | `word`, `sentence`, `paragraph` |
| `person` | Personal information | `firstName`, `lastName`, `fullName` |
| `music` | Music-related data | `genre`, `songName`, `artist` |
| `number` | Numeric data | `int`, `float`, `binary`, `hex` |
| `phone` | Phone numbers | `number`, `imei` |
| `science` | Scientific data | `chemicalElement`, `unit` |
| `string` | String utilities | `uuid`, `alpha`, `alphanumeric` |
| `system` | System-related data | `fileName`, `mimeType`, `fileExt` |
| `vehicle` | Vehicle information | `vehicle`, `manufacturer`, `model` |
| `word` | Word generation | `adjective`, `adverb`, `conjunction` |

View File

@@ -0,0 +1,23 @@
{
"name": "@yaak/faker",
"private": true,
"version": "0.1.0",
"displayName": "Faker",
"description": "Template functions for generating fake data using FakerJS",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins-external/faker"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
},
"dependencies": {
"@faker-js/faker": "^10.1.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,106 @@
import { faker } from '@faker-js/faker';
import type { PluginDefinition, TemplateFunctionArg } from '@yaakapp/api';
const modules = [
'airline',
'animal',
'color',
'commerce',
'company',
'database',
'date',
'finance',
'git',
'hacker',
'image',
'internet',
'location',
'lorem',
'person',
'music',
'number',
'phone',
'science',
'string',
'system',
'vehicle',
'word',
];
function normalizeResult(result: unknown): string {
if (typeof result === 'string') return result;
return JSON.stringify(result);
}
// Whatever Yaaks arg type shape is rough example
function args(modName: string, fnName: string): TemplateFunctionArg[] {
return [
{
type: 'banner',
color: 'info',
inputs: [
{
type: 'markdown',
content: `Need help? View documentation for [\`${modName}.${fnName}(…)\`](https://fakerjs.dev/api/${encodeURIComponent(modName)}.html#${encodeURIComponent(fnName)})`,
},
],
},
{
name: 'options',
label: 'Arguments',
type: 'editor',
language: 'json',
optional: true,
placeholder: 'e.g. { "min": 1, "max": 10 } or 10 or ["en","US"]',
},
];
}
export const plugin: PluginDefinition = {
templateFunctions: modules.flatMap((modName) => {
// @ts-expect-error - Dynamic access to faker modules
const mod = faker[modName];
return Object.keys(mod)
.filter((n) => n !== 'faker')
.map((fnName) => ({
name: ['faker', modName, fnName].join('.'),
args: args(modName, fnName),
async onRender(_ctx, args) {
const fn = mod[fnName] as (...a: unknown[]) => unknown;
const options = args.values.options;
// No options supplied
if (options == null || options === '') {
return normalizeResult(fn());
}
// Try JSON first
let parsed: unknown = options;
if (typeof options === 'string') {
try {
parsed = JSON.parse(options);
} catch {
// Not valid JSON maybe just a scalar
const n = Number(options);
if (!Number.isNaN(n)) {
parsed = n;
} else {
parsed = options;
}
}
}
let result: unknown;
if (Array.isArray(parsed)) {
// Treat as positional arguments
result = fn(...parsed);
} else {
// Treat as a single argument (option object or scalar)
result = fn(parsed);
}
return normalizeResult(result);
},
}));
}),
};

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -0,0 +1,54 @@
# Yaak MCP Server Plugin
A Yaak plugin that exposes Yaak's functionality via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants and other tools to interact with Yaak programmatically.
## Features
This plugin starts an MCP server on `http://127.0.0.1:64343/mcp` that provides tools for:
### HTTP Requests
- `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
### Folders
- `list_folders` - List all folders in a workspace
### Workspaces
- `list_workspaces` - List all open workspaces in Yaak
### Clipboard
- `copy_to_clipboard` - Copy text to the system clipboard
### Window
- `get_workspace_id` - Get the current workspace ID
- `get_environment_id` - Get the current environment ID
### Toast Notifications
- `show_toast` - Show a toast notification in Yaak
## Usage
Once the plugin is installed and Yaak is running, the MCP server will be available at:
```
http://127.0.0.1:64343/mcp
```
Configure your MCP client to connect to this endpoint to start interacting with Yaak.
## Development
```bash
# Install dependencies
npm install
# Build the plugin
npm run build
# Development mode with auto-rebuild
npm run dev
```

View File

@@ -0,0 +1,28 @@
{
"name": "@yaak/mcp-server",
"private": true,
"version": "0.1.0",
"displayName": "MCP Server",
"description": "Expose Yaak functionality via Model Context Protocol",
"minYaakVersion": "2025.10.0-beta.6",
"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.2.3",
"@hono/node-server": "^1.19.7",
"@modelcontextprotocol/sdk": "^1.25.1",
"hono": "^4.11.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^25.0.3",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,25 @@
import type { Context, PluginDefinition } from '@yaakapp/api';
import { createMcpServer } from './server.js';
const serverPort = 64343;
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.
setTimeout(() => {
mcpServer = createMcpServer({ yaak: ctx }, serverPort);
}, 5000);
},
async dispose() {
console.log('Disposing MCP Server plugin');
if (mcpServer) {
await mcpServer.close();
mcpServer = null;
}
},
};

View File

@@ -0,0 +1,58 @@
import { serve } from '@hono/node-server';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.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) {
const server = new McpServer({
name: 'yaak-mcp-server',
version: '0.1.0',
});
// Register all tools
registerToastTools(server, ctx);
registerHttpRequestTools(server, ctx);
registerFolderTools(server, ctx);
registerWindowTools(server, ctx);
registerWorkspaceTools(server, ctx);
// Create a stateless transport
const transport = new WebStandardStreamableHTTPServerTransport();
// Create Hono app
const app = new Hono();
// MCP endpoint - reuse the same transport for all requests
app.all('/mcp', (c) => transport.handleRequest(c.req.raw));
// Connect server to transport
server.connect(transport).then(() => {
console.log(`MCP Server running at http://127.0.0.1:${port}/mcp`);
ctx.yaak.toast.show({
message: `MCP Server running on port ${port}`,
icon: 'check_circle',
color: 'success',
});
});
// Start the HTTP server
const honoServer = serve({
fetch: app.fetch,
port,
hostname: '127.0.0.1',
});
return {
server,
close: async () => {
honoServer.close();
await server.close();
},
};
}

View File

@@ -0,0 +1,33 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4';
import type { McpServerContext } from '../types.js';
import { getWorkspaceContext } from './helpers.js';
export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
server.registerTool(
'list_folders',
{
title: 'List Folders',
description: 'List all folders in a workspace',
inputSchema: z.object({
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
}),
},
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),
},
],
};
},
);
}

View 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),
};
}

View File

@@ -0,0 +1,219 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4';
import type { McpServerContext } from '../types.js';
import { getWorkspaceContext } from './helpers.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: z.object({
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
}),
},
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: z.object({
id: z.string().describe('The HTTP request ID'),
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
}),
},
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: z.object({
id: z.string().describe('The HTTP request ID to send'),
environmentId: z.string().optional().describe('Optional environment ID to use'),
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
}),
},
async ({ id, workspaceId }) => {
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 response = await workspaceCtx.yaak.httpRequest.send({ httpRequest });
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: z.object({
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
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: z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional()
.describe('Request headers'),
urlParameters: z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional()
.describe('URL query parameters'),
}),
},
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: z.object({
id: z.string().describe('HTTP request ID to update'),
workspaceId: z
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
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: z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional()
.describe('Request headers'),
urlParameters: z
.array(
z.object({
name: z.string(),
value: z.string(),
enabled: z.boolean().default(true),
}),
)
.optional()
.describe('URL query parameters'),
}),
},
async ({ id, workspaceId, ...updates }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const httpRequest = await workspaceCtx.yaak.httpRequest.update({ id, ...updates });
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: z.object({
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})` },
],
};
},
);
}

View 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/v4';
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: z.object({
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}"`,
},
],
};
},
);
}

View File

@@ -0,0 +1,50 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4';
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',
inputSchema: z.object({}),
},
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',
inputSchema: z.object({}),
},
async () => {
const workspaceCtx = await getWorkspaceContext(ctx);
const environmentId = await workspaceCtx.yaak.window.environmentId();
return {
content: [
{
type: 'text' as const,
text: environmentId || 'No environment selected',
},
],
};
},
);
}

View File

@@ -0,0 +1,26 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4';
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',
inputSchema: z.object({}),
},
async () => {
const workspaces = await ctx.yaak.workspace.list();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(workspaces, null, 2),
},
],
};
},
);
}

View File

@@ -0,0 +1,5 @@
import type { Context } from '@yaakapp/api';
export interface McpServerContext {
yaak: Context;
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}