Skip to main content
Glama

Scratchpad MCP

by pc035860
server.ts24.6 kB
/** * Scratchpad MCP Server - A simplified context sharing server for Claude Code sub-agents */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { ScratchpadDatabase } from './database/index.js'; import { createWorkflowTool, listWorkflowsTool, getLatestActiveWorkflowTool, getWorkflowTool, updateWorkflowStatusTool, createScratchpadTool, getScratchpadTool, getScratchpadOutlineTool, appendScratchpadTool, tailScratchpadTool, chopScratchpadTool, enhancedUpdateScratchpadTool, listScratchpadsTool, searchScratchpadContentTool, searchWorkflowsTool, extractWorkflowInfoTool, } from './tools/index.js'; import { handleToolError, createToolResponse, validateTailScratchpadArgs, } from './server-helpers.js'; // Helper function to filter undefined values from objects function filterUndefined<T extends Record<string, any>>(obj: T): Partial<T> { const filtered: Partial<T> = {}; for (const [key, value] of Object.entries(obj)) { if (value !== undefined) { (filtered as any)[key] = value; } } return filtered; } class ScratchpadMCPServer { private server: McpServer; private db: ScratchpadDatabase; private disabledTools: Set<string>; constructor() { this.server = new McpServer( { name: 'scratchpad-mcp-v2', version: '1.0.0', } ); // Initialize database const dbPath = process.env['SCRATCHPAD_DB_PATH'] || './scratchpad.db'; this.db = new ScratchpadDatabase({ filename: dbPath }); // Parse disabled tools from environment variable this.disabledTools = this.parseDisabledTools(); this.setupToolHandlers(); // Handle cleanup on exit process.on('SIGINT', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); } private parseDisabledTools(): Set<string> { const disabled = process.env['SCRATCHPAD_DISABLED_TOOLS'] || ''; return new Set( disabled .split(',') .map(s => s.trim()) .filter(s => s.length > 0) ); } private setupToolHandlers(): void { // Workflow management tools this.server.registerTool('create-workflow', { title: 'Create Workflow', description: 'Create a new workflow for organizing scratchpads', inputSchema: { name: z.string().describe('Name of the workflow'), description: z.string().optional().describe('Optional description of the workflow'), project_scope: z.string().optional().describe('Optional project scope to isolate workflows by project'), } }, async ({ name, description, project_scope }) => { try { const createWorkflowFn = createWorkflowTool(this.db); const result = await createWorkflowFn(filterUndefined({ name, description, project_scope }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'create-workflow'); } }); this.server.registerTool('list-workflows', { title: 'List Workflows', description: 'List all available workflows', inputSchema: { project_scope: z.string().optional().describe('Optional project scope to filter workflows by project'), limit: z.number().min(1).max(100).optional().describe('Maximum number of workflows to return (default: 20, max: 100)'), offset: z.number().min(0).optional().describe('Number of workflows to skip for pagination (default: 0)'), preview_mode: z.boolean().optional().describe('Preview mode - truncate workflow descriptions for brevity'), max_content_chars: z.number().min(10).optional().describe('Maximum characters for workflow description content'), include_content: z.boolean().optional().describe('Whether to include workflow descriptions in response'), } }, async ({ project_scope, limit, offset, preview_mode, max_content_chars, include_content }) => { try { const listWorkflowsFn = listWorkflowsTool(this.db); const result = await listWorkflowsFn(filterUndefined({ project_scope, limit, offset, preview_mode, max_content_chars, include_content }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'list-workflows'); } }); this.server.registerTool('get-latest-active-workflow', { title: 'Get Latest Active Workflow', description: 'Get the most recently updated active workflow', inputSchema: { project_scope: z.string().describe('Project scope to filter workflows by project'), } }, async ({ project_scope }) => { try { const getLatestActiveWorkflowFn = getLatestActiveWorkflowTool(this.db); const result = await getLatestActiveWorkflowFn({ project_scope }); return createToolResponse(result); } catch (error) { return handleToolError(error, 'get-latest-active-workflow'); } }); this.server.registerTool('get-workflow', { title: 'Get Workflow', description: 'Retrieve a workflow by its ID with optional scratchpads summary', inputSchema: { workflow_id: z.string().describe('ID of the workflow to retrieve'), include_scratchpads_summary: z.boolean().optional().describe('Whether to include scratchpads summary (default: true)'), } }, async ({ workflow_id, include_scratchpads_summary }) => { try { const getWorkflowFn = getWorkflowTool(this.db); const result = await getWorkflowFn(filterUndefined({ workflow_id, include_scratchpads_summary }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'get-workflow'); } }); this.server.registerTool('update-workflow-status', { title: 'Update Workflow Status', description: 'Activate or deactivate a workflow. Only active workflows can have scratchpads created or modified.', inputSchema: { workflow_id: z.string().describe('ID of the workflow to update'), is_active: z.boolean().describe('Set to true to activate, false to deactivate the workflow'), } }, async ({ workflow_id, is_active }) => { try { const updateWorkflowStatusFn = updateWorkflowStatusTool(this.db); const result = await updateWorkflowStatusFn({ workflow_id, is_active }); return createToolResponse(result); } catch (error) { return handleToolError(error, 'update-workflow-status'); } }); // Scratchpad CRUD tools this.server.registerTool('create-scratchpad', { title: 'Create Scratchpad', description: 'Create a new scratchpad in a workflow', inputSchema: { workflow_id: z.string().describe('ID of the workflow to add the scratchpad to'), title: z.string().describe('Title of the scratchpad'), content: z.string().describe('Content of the scratchpad'), include_content: z.boolean().optional().describe('Whether to return full content in response (default: false, returns metadata only)'), } }, async ({ workflow_id, title, content, include_content }) => { try { const createScratchpadFn = createScratchpadTool(this.db); const result = await createScratchpadFn(filterUndefined({ workflow_id, title, content, include_content }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'create-scratchpad'); } }); const getScratchpadToolInstance = this.server.registerTool('get-scratchpad', { title: 'Get Scratchpad', description: 'Retrieve a scratchpad by its ID with optional range selection. Supports line_range (specific lines) and line_context (line + surrounding context or block). Content truncated to 2000 chars by default.', inputSchema: { id: z.string().describe('ID of the scratchpad to retrieve'), line_range: z.object({ start: z.number().min(1).describe('Starting line number (1-based)'), end: z.number().min(1).optional().describe('Ending line number (optional, defaults to end of file)'), }).optional().describe('Specify line range to extract'), line_context: z.object({ line: z.number().min(1).describe('Target line number (1-based)'), before: z.number().min(0).optional().describe('Lines before target line (default: 2)'), after: z.number().min(0).optional().describe('Lines after target line (default: 2)'), include_block: z.boolean().optional().describe('Return entire block containing the target line (ignores before/after)'), }).optional().describe('Specify line with surrounding context'), preview_mode: z.boolean().optional().describe('Preview mode - returns ~200 chars with smart truncation. Takes precedence over max_content_chars.'), max_content_chars: z.number().min(10).optional().describe('Maximum characters limit (default: 2000). Only applies when include_content is not false.'), include_content: z.boolean().optional().describe('Whether to include content in response. false=metadata only (overrides other content options), true=include content.'), } }, async ({ id, line_range, line_context, preview_mode, max_content_chars, include_content }) => { try { const getScratchpadFn = getScratchpadTool(this.db); const result = await getScratchpadFn(filterUndefined({ id, line_range, line_context, preview_mode, max_content_chars, include_content }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'get-scratchpad'); } }); // Conditionally disable get-scratchpad tool if (this.disabledTools.has('get-scratchpad')) { getScratchpadToolInstance.disable(); } const getScratchpadOutlineToolInstance = this.server.registerTool('get-scratchpad-outline', { title: 'Get Scratchpad Outline', description: 'Parse and retrieve the markdown header structure of a scratchpad. Shows hierarchy with line numbers to help locate content sections.', inputSchema: { id: z.string().describe('ID of the scratchpad to analyze'), max_depth: z.number().min(1).max(6).optional().describe('Maximum header depth to include (1-6, default: unlimited)'), include_line_numbers: z.boolean().optional().describe('Whether to include line numbers in output (default: true)'), include_content_preview: z.boolean().optional().describe('Whether to include content preview for each section (default: false)'), } }, async ({ id, max_depth, include_line_numbers, include_content_preview }) => { try { const getScratchpadOutlineFn = getScratchpadOutlineTool(this.db); const result = await getScratchpadOutlineFn(filterUndefined({ id, max_depth, include_line_numbers, include_content_preview }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'get-scratchpad-outline'); } }); // Conditionally disable get-scratchpad-outline tool if (this.disabledTools.has('get-scratchpad-outline')) { getScratchpadOutlineToolInstance.disable(); } this.server.registerTool('append-scratchpad', { title: 'Append Scratchpad', description: 'Append content to an existing scratchpad', inputSchema: { id: z.string().describe('ID of the scratchpad to append to'), content: z.string().describe('Content to append to the scratchpad'), include_content: z.boolean().optional().describe('Whether to return full content in response (default: false, returns metadata only)'), } }, async ({ id, content, include_content }) => { try { const appendScratchpadFn = appendScratchpadTool(this.db); const result = await appendScratchpadFn(filterUndefined({ id, content, include_content }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'append-scratchpad'); } }); this.server.registerTool('tail-scratchpad', { title: 'Tail Scratchpad', description: 'Get tail content from scratchpad (default: last 50 lines), or full content with full_content=true. Use include_content=false for metadata only.', inputSchema: { id: z.string().describe('ID of the scratchpad to get tail from'), tail_size: z.object({ lines: z.number().min(1).optional().describe('Number of lines to return from the end'), chars: z.number().min(1).optional().describe('Number of characters to return from the end'), blocks: z.number().min(1).optional().describe('Number of blocks to return from the end') }).refine(data => { const fields = [data.lines, data.chars, data.blocks].filter(v => v !== undefined); return fields.length === 1; }, { message: "Exactly one of lines, chars, or blocks must be specified" }).optional().describe('Tail size specification - choose either lines OR chars OR blocks, not multiple'), include_content: z.boolean().optional().describe('Whether to include content in response (default: true)'), full_content: z.boolean().optional().describe('Whether to return full content instead of tail (overrides tail_size). Use this as alternative to get-scratchpad.'), } }, async ({ id, tail_size, include_content, full_content }) => { try { const tailScratchpadFn = tailScratchpadTool(this.db); const validatedArgs = validateTailScratchpadArgs({ id, tail_size, include_content, full_content }); const result = await tailScratchpadFn(validatedArgs); return createToolResponse(result); } catch (error) { return handleToolError(error, 'tail-scratchpad'); } }); this.server.registerTool('chop-scratchpad', { title: 'Chop Scratchpad', description: 'Remove lines or blocks from the end of a scratchpad. Does not return content after completion.', inputSchema: { id: z.string().describe('ID of the scratchpad to chop content from'), lines: z.number().min(1).optional().describe('Number of lines to remove from the end (default: 1)'), blocks: z.number().min(1).optional().describe('Number of blocks to remove from the end'), } }, async ({ id, lines, blocks }) => { try { const chopScratchpadFn = chopScratchpadTool(this.db); const result = await chopScratchpadFn(filterUndefined({ id, lines, blocks }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'chop-scratchpad'); } }); this.server.registerTool('update-scratchpad', { title: 'Update Scratchpad', description: 'Enhanced multi-mode scratchpad editing tool. Supports four editing modes: replace (complete replacement), insert_at_line (insert at specific line), replace_lines (replace line range), append_section (smart append after markdown section marker). Provides detailed operation feedback.', inputSchema: { id: z.string().describe('ID of the scratchpad to edit'), mode: z.enum(['replace', 'insert_at_line', 'replace_lines', 'append_section']).describe('Editing mode to use'), content: z.string().describe('Content to insert, replace, or append'), include_content: z.boolean().optional().describe('Whether to include content in response (default: false)'), line_number: z.number().min(1).optional().describe('Line number for insert_at_line mode (1-based indexing)'), start_line: z.number().min(1).optional().describe('Start line for replace_lines mode (1-based, inclusive)'), end_line: z.number().min(1).optional().describe('End line for replace_lines mode (1-based, inclusive)'), section_marker: z.string().optional().describe('Section marker for append_section mode (e.g., "## Features", "# TODO")'), } }, async ({ id, mode, content, include_content, line_number, start_line, end_line, section_marker }) => { try { const enhancedUpdateScratchpadFn = enhancedUpdateScratchpadTool(this.db); const result = await enhancedUpdateScratchpadFn(filterUndefined({ id, mode, content, include_content, line_number, start_line, end_line, section_marker }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'update-scratchpad'); } }); this.server.registerTool('list-scratchpads', { title: 'List Scratchpads', description: 'List scratchpads in a workflow. Options: preview_mode (quick overview), max_content_chars (size limit), include_content=false (metadata only). Priority: include_content > preview_mode > max_content_chars.', inputSchema: { workflow_id: z.string().describe('ID of the workflow to list scratchpads from'), limit: z.number().min(1).max(50).optional().describe('Maximum number of scratchpads to return (default: 20, max: 50)'), offset: z.number().min(0).optional().describe('Number of scratchpads to skip (default: 0)'), preview_mode: z.boolean().optional().describe('Preview mode - return truncated content for brevity'), max_content_chars: z.number().min(10).optional().describe('Maximum characters per scratchpad content'), include_content: z.boolean().optional().describe('Whether to include full content in response'), } }, async ({ workflow_id, limit, offset, preview_mode, max_content_chars, include_content }) => { try { const listScratchpadsFn = listScratchpadsTool(this.db); const result = await listScratchpadsFn(filterUndefined({ workflow_id, limit, offset, preview_mode, max_content_chars, include_content }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'list-scratchpads'); } }); this.server.registerTool('search-scratchpad-content', { title: 'Search Scratchpad Content', description: 'Search within a single scratchpad content using string or regex patterns. Similar to VS Code Ctrl+F or grep for a single file. Supports context-aware search results with line-based context.', inputSchema: { id: z.string().describe('ID of the scratchpad to search within'), query: z.string().optional().describe('String search pattern (cannot be used with queryRegex)'), queryRegex: z.string().optional().describe('Regular expression search pattern (cannot be used with query)'), preview_mode: z.boolean().optional().describe('Preview mode - return truncated content for search results'), max_content_chars: z.number().min(10).optional().describe('Maximum characters per match snippet'), include_content: z.boolean().optional().describe('Whether to include content in search results'), context_lines_before: z.number().min(0).max(50).optional().describe('Number of lines to show before each match (0-50)'), context_lines_after: z.number().min(0).max(50).optional().describe('Number of lines to show after each match (0-50)'), context_lines: z.number().min(0).max(50).optional().describe('Number of lines to show both before and after each match (shorthand, 0-50)'), max_context_matches: z.number().min(1).max(20).optional().describe('Maximum number of matches to show context for (default: 5, max: 20)'), merge_context: z.boolean().optional().describe('Whether to merge overlapping context ranges (default: true)'), show_line_numbers: z.boolean().optional().describe('Whether to show line numbers in context output'), } }, async ({ id, query, queryRegex, preview_mode, max_content_chars, include_content, context_lines_before, context_lines_after, context_lines, max_context_matches, merge_context, show_line_numbers }) => { try { const searchScratchpadContentFn = searchScratchpadContentTool(this.db); const result = await searchScratchpadContentFn(filterUndefined({ id, query, queryRegex, preview_mode, max_content_chars, include_content, context_lines_before, context_lines_after, context_lines, max_context_matches, merge_context, show_line_numbers }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'search-scratchpad-content'); } }); this.server.registerTool('search-workflows', { title: 'Search Workflows', description: 'Search workflows with weighted scoring based on scratchpads content. Uses the "search workflows first, then load scratchpads" strategy with 5/3/3/1 weighted scoring (workflows.name/description, scratchpads.title/content). Supports mixed English/Chinese queries with automatic language separation and 4-tier fallback (FTS5 → LIKE).\n\nFEATURES:\n• Weighted scoring: workflows.name (5pts), workflows.description (3pts), scratchpads.title (3pts), scratchpads.content (1pt)\n• Mixed language support: "React組件" → English: ["React"] + Chinese: ["組件"]\n• Project isolation: project_scope parameter for exact match filtering\n• Smart pagination: 5 items per page (default: page 1)\n• Comprehensive search: workflows + ALL scratchpads content under those workflows\n• Performance optimized: search workflows first, then load scratchpads for scoring\n\nUSAGE EXAMPLES:\n• Basic search: {"query": "authentication"}\n• Project-scoped: {"query": "user login", "project_scope": "myapp"}\n• Pagination: {"query": "React", "page": 2, "limit": 10}\n• Mixed language: {"query": "React組件開發", "useJieba": true}\n• English-only: {"query": "database connection pool"}', inputSchema: { query: z.string().describe('Search query string (supports mixed English/Chinese)'), project_scope: z.string().optional().describe('Optional project scope for exact match filtering (isolates workflows by project)'), page: z.number().min(1).optional().describe('Page number for pagination (default: 1)'), limit: z.number().min(1).max(20).optional().describe('Items per page (default: 5, max: 20)'), useJieba: z.boolean().optional().describe('Force jieba Chinese tokenization (auto-detect by default)'), } }, async ({ query, project_scope, page, limit, useJieba }) => { try { const searchWorkflowsFn = searchWorkflowsTool(this.db); const result = await searchWorkflowsFn(filterUndefined({ query, project_scope, page, limit, useJieba }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'search-workflows'); } }); this.server.registerTool('extract-workflow-info', { title: 'Extract Workflow Info', description: 'Extract specific information from a workflow using OpenAI model', inputSchema: { workflow_id: z.string().describe('ID of the workflow to extract information from'), extraction_prompt: z.string().describe('Specific prompt describing what information to extract'), model: z.string().optional().describe('OpenAI model to use (default: gpt-5-nano)'), reasoning_effort: z.enum(['minimal', 'low', 'medium', 'high']).optional().describe('Reasoning effort level for GPT-5 models (default: medium)'), } }, async ({ workflow_id, extraction_prompt, model, reasoning_effort }) => { try { const extractWorkflowInfoFn = extractWorkflowInfoTool(this.db); const result = await extractWorkflowInfoFn(filterUndefined({ workflow_id, extraction_prompt, model, reasoning_effort }) as any); return createToolResponse(result); } catch (error) { return handleToolError(error, 'extract-workflow-info'); } }); } private cleanup(): void { console.error('Shutting down Scratchpad MCP Server...'); this.db.close(); process.exit(0); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Scratchpad MCP Server running on stdio'); } } // Start the server if this file is run directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new ScratchpadMCPServer(); server.run().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pc035860/scratchpad-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server