diff --git a/packages/plugin-runtime-types/src/plugins/Context.ts b/packages/plugin-runtime-types/src/plugins/Context.ts index ad880ae7..1e0a26ff 100644 --- a/packages/plugin-runtime-types/src/plugins/Context.ts +++ b/packages/plugin-runtime-types/src/plugins/Context.ts @@ -25,7 +25,7 @@ import type { TemplateRenderRequest, WorkspaceInfo, } from '../bindings/gen_events.ts'; -import type { HttpRequest } from '../bindings/gen_models.ts'; +import type { Folder, HttpRequest } from '../bindings/gen_models.ts'; import type { JsonValue } from '../bindings/serde_json/JsonValue'; export type WorkspaceHandle = Pick; @@ -82,6 +82,15 @@ export interface Context { }; folder: { list(args?: ListFoldersRequest): Promise; + getById(args: { id: string }): Promise; + create( + args: Omit, 'id' | 'model' | 'createdAt' | 'updatedAt'> & + Pick, + ): Promise; + update( + args: Omit, 'model' | 'createdAt' | 'updatedAt'> & Pick, + ): Promise; + delete(args: { id: string }): Promise; }; httpResponse: { find(args: FindHttpResponsesRequest): Promise; diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 52e81d70..36e288ba 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -11,6 +11,7 @@ import type { DeleteKeyValueResponse, DeleteModelResponse, FindHttpResponsesResponse, + Folder, GetCookieValueRequest, GetCookieValueResponse, GetHttpRequestByIdResponse, @@ -782,6 +783,44 @@ export class PluginInstance { const { folders } = await this.#sendForReply(context, payload); return folders; }, + getById: async (args: { id: string }) => { + const payload = { type: 'list_folders_request' } as const; + const { folders } = await this.#sendForReply(context, payload); + return folders.find((f) => f.id === args.id) ?? null; + }, + create: async (args) => { + const payload = { + type: 'upsert_model_request', + model: { + name: '', + ...args, + id: '', + model: 'folder', + }, + } as InternalEventPayload; + const response = await this.#sendForReply(context, payload); + return response.model as Folder; + }, + update: async (args) => { + const payload = { + type: 'upsert_model_request', + model: { + model: 'folder', + ...args, + }, + } as InternalEventPayload; + const response = await this.#sendForReply(context, payload); + return response.model as Folder; + }, + delete: async (args: { id: string }) => { + const payload = { + type: 'delete_model_request', + model: 'folder', + id: args.id, + } as InternalEventPayload; + const response = await this.#sendForReply(context, payload); + return response.model as Folder; + }, }, cookies: { getValue: async (args: GetCookieValueRequest) => { diff --git a/plugins-external/mcp-server/src/tools/folder.ts b/plugins-external/mcp-server/src/tools/folder.ts index 7f8d92b2..f6bf068d 100644 --- a/plugins-external/mcp-server/src/tools/folder.ts +++ b/plugins-external/mcp-server/src/tools/folder.ts @@ -2,6 +2,12 @@ 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( @@ -10,10 +16,7 @@ export function registerFolderTools(server: McpServer, ctx: McpServerContext) { title: 'List Folders', description: 'List all folders in a workspace', inputSchema: { - workspaceId: z - .string() - .optional() - .describe('Workspace ID (required if multiple workspaces are open)'), + workspaceId: workspaceIdSchema, }, }, async ({ workspaceId }) => { @@ -30,4 +33,116 @@ export function registerFolderTools(server: McpServer, ctx: McpServerContext) { }; }, ); + + 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})` }], + }; + }, + ); } diff --git a/plugins-external/mcp-server/src/tools/httpRequest.ts b/plugins-external/mcp-server/src/tools/httpRequest.ts index 0bec8e07..5effa654 100644 --- a/plugins-external/mcp-server/src/tools/httpRequest.ts +++ b/plugins-external/mcp-server/src/tools/httpRequest.ts @@ -2,6 +2,15 @@ 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( @@ -10,10 +19,7 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex title: 'List HTTP Requests', description: 'List all HTTP requests in a workspace', inputSchema: { - workspaceId: z - .string() - .optional() - .describe('Workspace ID (required if multiple workspaces are open)'), + workspaceId: workspaceIdSchema, }, }, async ({ workspaceId }) => { @@ -38,10 +44,7 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex description: 'Get details of a specific HTTP request by ID', inputSchema: { id: z.string().describe('The HTTP request ID'), - workspaceId: z - .string() - .optional() - .describe('Workspace ID (required if multiple workspaces are open)'), + workspaceId: workspaceIdSchema, }, }, async ({ id, workspaceId }) => { @@ -67,10 +70,7 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex inputSchema: { 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)'), + workspaceId: workspaceIdSchema, }, }, async ({ id, workspaceId }) => { @@ -99,10 +99,7 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex title: 'Create HTTP Request', description: 'Create a new HTTP request', inputSchema: { - workspaceId: z - .string() - .optional() - .describe('Workspace ID (required if multiple workspaces are open)'), + workspaceId: workspaceIdSchema, name: z .string() .optional() @@ -111,62 +108,12 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex 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'), - bodyType: 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")', - ), - body: 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" }', - ), - authenticationType: z - .string() - .optional() - .describe( - 'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent folder/workspace.', - ), - authentication: 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": {}', - ), + headers: headersSchema.describe('Request headers'), + urlParameters: urlParametersSchema, + bodyType: bodyTypeSchema, + body: bodySchema, + authenticationType: authenticationTypeSchema, + authentication: authenticationSchema, }, }, async ({ workspaceId: ogWorkspaceId, ...args }) => { @@ -194,68 +141,18 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex description: 'Update an existing HTTP request', inputSchema: { id: z.string().describe('HTTP request ID to update'), - workspaceId: z.string().describe('Workspace ID'), + 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: 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'), - bodyType: 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")', - ), - body: 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" }', - ), - authenticationType: z - .string() - .optional() - .describe( - 'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent folder/workspace.', - ), - authentication: 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": {}', - ), + headers: headersSchema.describe('Request headers'), + urlParameters: urlParametersSchema, + bodyType: bodyTypeSchema, + body: bodySchema, + authenticationType: authenticationTypeSchema, + authentication: authenticationSchema, }, }, async ({ id, workspaceId, ...updates }) => { diff --git a/plugins-external/mcp-server/src/tools/schemas.ts b/plugins-external/mcp-server/src/tools/schemas.ts new file mode 100644 index 00000000..7a1cb4ab --- /dev/null +++ b/plugins-external/mcp-server/src/tools/schemas.ts @@ -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": {}', + );