Add body and auth support to MCP HTTP request tools

- Add body, bodyType, authentication, and authenticationType fields to create/update HTTP request MCP tools
- Include comprehensive documentation for body structures and auth types in Zod descriptions
- Fix MCP update_http_request to merge partial updates with existing data to prevent FK constraint violations
- Fix Zod imports from 'zod/v4' to 'zod' to match installed version
- Add uncaughtException handler to plugin runtime to prevent individual plugin crashes from crashing entire runtime
This commit is contained in:
Gregory Schier
2026-01-01 10:33:28 -08:00
parent 92a8da03af
commit d544899f39
9 changed files with 105 additions and 20 deletions
@@ -8,7 +8,9 @@ Generate formatted release notes for Yaak releases by analyzing git history and
## What to do ## What to do
1. Identifies the version tag and previous version 1. Identifies the version tag and previous version
2. Retrieves all commits between versions (scoped to stable or beta) 2. Retrieves all commits between versions
- If the version is a beta version, it retrieves commits between the beta version and previous beta version
- If the version is a stable version, it retrieves commits between the stable version and the previous stable version
3. Fetches PR descriptions for linked issues to find: 3. Fetches PR descriptions for linked issues to find:
- Feedback URLs (feedback.yaak.app) - Feedback URLs (feedback.yaak.app)
- Additional context and descriptions - Additional context and descriptions
@@ -1,3 +1,7 @@
import console from 'node:console';
import { type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
import type { Context, PluginDefinition } from '@yaakapp/api';
import { import {
applyFormInputDefaults, applyFormInputDefaults,
validateTemplateFunctionArgs, validateTemplateFunctionArgs,
@@ -34,10 +38,6 @@ import type {
UpsertModelResponse, UpsertModelResponse,
WindowInfoResponse, WindowInfoResponse,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import type { Context, PluginDefinition } from '@yaakapp/api';
import console from 'node:console';
import { type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
import { applyDynamicFormInput } from './common'; import { applyDynamicFormInput } from './common';
import { EventChannel } from './EventChannel'; import { EventChannel } from './EventChannel';
import { migrateTemplateFunctionSelectOptions } from './migrations'; import { migrateTemplateFunctionSelectOptions } from './migrations';
+5 -1
View File
@@ -1,7 +1,7 @@
import type { InternalEvent } from '@yaakapp/api'; import type { InternalEvent } from '@yaakapp/api';
import WebSocket from 'ws';
import { EventChannel } from './EventChannel'; import { EventChannel } from './EventChannel';
import { PluginHandle } from './PluginHandle'; import { PluginHandle } from './PluginHandle';
import WebSocket from 'ws';
const port = process.env.PORT; const port = process.env.PORT;
if (!port) { if (!port) {
@@ -67,3 +67,7 @@ async function handleIncoming(msg: string) {
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason); console.error('Unhandled Rejection at:', promise, 'reason:', reason);
}); });
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
});
@@ -1,5 +1,5 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4'; import * as z from 'zod';
import type { McpServerContext } from '../types.js'; import type { McpServerContext } from '../types.js';
import { getWorkspaceContext } from './helpers.js'; import { getWorkspaceContext } from './helpers.js';
@@ -1,5 +1,5 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4'; import * as z from 'zod';
import type { McpServerContext } from '../types.js'; import type { McpServerContext } from '../types.js';
import { getWorkspaceContext } from './helpers.js'; import { getWorkspaceContext } from './helpers.js';
@@ -131,6 +131,42 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
) )
.optional() .optional()
.describe('URL query parameters'), .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": {}',
),
}), }),
}, },
async ({ workspaceId: ogWorkspaceId, ...args }) => { async ({ workspaceId: ogWorkspaceId, ...args }) => {
@@ -158,10 +194,7 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
description: 'Update an existing HTTP request', description: 'Update an existing HTTP request',
inputSchema: z.object({ inputSchema: z.object({
id: z.string().describe('HTTP request ID to update'), id: z.string().describe('HTTP request ID to update'),
workspaceId: z workspaceId: z.string().describe('Workspace ID'),
.string()
.optional()
.describe('Workspace ID (required if multiple workspaces are open)'),
name: z.string().optional().describe('Request name'), name: z.string().optional().describe('Request name'),
url: z.string().optional().describe('Request URL'), url: z.string().optional().describe('Request URL'),
method: z.string().optional().describe('HTTP method'), method: z.string().optional().describe('HTTP method'),
@@ -187,11 +220,57 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
) )
.optional() .optional()
.describe('URL query parameters'), .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": {}',
),
}), }),
}, },
async ({ id, workspaceId, ...updates }) => { async ({ id, workspaceId, ...updates }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId); const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const httpRequest = await workspaceCtx.yaak.httpRequest.update({ id, ...updates }); // 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 { return {
content: [{ type: 'text' as const, text: JSON.stringify(httpRequest, null, 2) }], content: [{ type: 'text' as const, text: JSON.stringify(httpRequest, null, 2) }],
}; };
@@ -1,6 +1,6 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { Color, Icon } from '@yaakapp/api'; import type { Color, Icon } from '@yaakapp/api';
import * as z from 'zod/v4'; import * as z from 'zod';
import type { McpServerContext } from '../types.js'; import type { McpServerContext } from '../types.js';
const ICON_VALUES = [ const ICON_VALUES = [
@@ -1,5 +1,5 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4'; import * as z from 'zod';
import type { McpServerContext } from '../types.js'; import type { McpServerContext } from '../types.js';
import { getWorkspaceContext } from './helpers.js'; import { getWorkspaceContext } from './helpers.js';
@@ -1,5 +1,5 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4'; import * as z from 'zod';
import type { McpServerContext } from '../types.js'; import type { McpServerContext } from '../types.js';
export function registerWorkspaceTools(server: McpServer, ctx: McpServerContext) { export function registerWorkspaceTools(server: McpServer, ctx: McpServerContext) {
@@ -10,9 +10,9 @@ export const synthwave84: Theme = {
text: 'hsl(300, 50%, 90%)', text: 'hsl(300, 50%, 90%)',
textSubtle: 'hsl(280, 25%, 65%)', textSubtle: 'hsl(280, 25%, 65%)',
textSubtlest: 'hsl(280, 20%, 50%)', textSubtlest: 'hsl(280, 20%, 50%)',
primary: 'hsl(177, 100%, 55%)', primary: 'hsl(320, 100%, 75%)',
secondary: 'hsl(280, 20%, 60%)', secondary: 'hsl(280, 20%, 60%)',
info: 'hsl(320, 100%, 75%)', info: 'hsl(177, 100%, 55%)',
success: 'hsl(83, 100%, 60%)', success: 'hsl(83, 100%, 60%)',
notice: 'hsl(57, 100%, 60%)', notice: 'hsl(57, 100%, 60%)',
warning: 'hsl(30, 100%, 60%)', warning: 'hsl(30, 100%, 60%)',
@@ -35,9 +35,9 @@ export const synthwave84: Theme = {
border: 'hsl(253, 40%, 22%)', border: 'hsl(253, 40%, 22%)',
}, },
button: { button: {
primary: 'hsl(177, 100%, 48%)', primary: 'hsl(320, 100%, 68%)',
secondary: 'hsl(280, 20%, 53%)', secondary: 'hsl(280, 20%, 53%)',
info: 'hsl(320, 100%, 68%)', info: 'hsl(177, 100%, 48%)',
success: 'hsl(83, 100%, 53%)', success: 'hsl(83, 100%, 53%)',
notice: 'hsl(57, 100%, 53%)', notice: 'hsl(57, 100%, 53%)',
warning: 'hsl(30, 100%, 53%)', warning: 'hsl(30, 100%, 53%)',