import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
export function registerHttpdHandlerScaffoldTool(server: McpServer): void {
server.registerTool(
'gerbil_httpd_handler_scaffold',
{
title: 'HTTP Handler Scaffold',
description:
'Generate an HTTP server handler skeleton using :std/net/httpd from route specifications. ' +
'Produces a complete server setup with routing, JSON request/response helpers, ' +
'error handling, and graceful shutdown.',
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
inputSchema: {
routes: z
.array(
z.object({
method: z.string().describe('HTTP method (GET, POST, PUT, DELETE)'),
path: z.string().describe('URL path pattern (e.g. /users, /users/:id)'),
handler: z.string().describe('Handler function name'),
}),
)
.describe('Route specifications'),
port: z
.number()
.optional()
.describe('Server port (default: 8080)'),
module_name: z
.string()
.optional()
.describe('Module name for the generated code'),
},
},
async ({ routes, port, module_name }) => {
const serverPort = port || 8080;
const modName = module_name || 'my-server';
const sections: string[] = [];
// Generate imports
sections.push(`;;; ${modName} — HTTP server generated by gerbil_httpd_handler_scaffold`);
sections.push('(import :std/net/httpd');
sections.push(' :std/text/json');
sections.push(' :std/sugar');
sections.push(' :std/format');
sections.push(' :std/logger');
sections.push(' :std/getopt)');
sections.push('');
sections.push(`(export main)`);
sections.push('');
// JSON helpers
sections.push(';;; JSON response helper');
sections.push('(def (json-response req data (status 200))');
sections.push(' (http-response-write req status');
sections.push(' \'((\"Content-Type\" . \"application/json\"))');
sections.push(' (json-object->string data)))');
sections.push('');
sections.push('(def (json-error req message (status 400))');
sections.push(' (json-response req (hash (\"error\" message)) status))');
sections.push('');
// Request body parser
sections.push(';;; Parse JSON request body');
sections.push('(def (parse-json-body req)');
sections.push(' (let ((body (http-request-body req)))');
sections.push(' (if body');
sections.push(' (with-catch');
sections.push(' (lambda (e) #f)');
sections.push(' (lambda () (string->json-object (bytes->string body))))');
sections.push(' #f)))');
sections.push('');
// Path parameter extraction
sections.push(';;; Extract path parameter');
sections.push('(def (path-param req pattern)');
sections.push(' (let ((path (http-request-path req)))');
sections.push(' ;; Simple path matching — extract segment after prefix');
sections.push(' (and (string-prefix? pattern path)');
sections.push(' (substring path (string-length pattern) (string-length path)))))');
sections.push('');
// Generate handler functions
for (const route of routes) {
sections.push(`;;; ${route.method} ${route.path}`);
sections.push(`(def (${route.handler} req)`);
if (route.method === 'GET' && route.path.includes(':')) {
// Handler with path params
const prefix = route.path.replace(/:[^/]+$/, '');
sections.push(` (let ((id (path-param req "${prefix}")))`);
sections.push(` (if id`);
sections.push(` (json-response req (hash ("id" id) ("status" "ok")))`);
sections.push(` (json-error req "Not found" 404))))`);
} else if (route.method === 'POST' || route.method === 'PUT') {
sections.push(' (let ((body (parse-json-body req)))');
sections.push(' (if body');
sections.push(' (begin');
sections.push(` ;; TODO: Process ${route.method} request body`);
sections.push(' (json-response req (hash ("status" "created")) 201))');
sections.push(' (json-error req "Invalid JSON body"))))');
} else if (route.method === 'DELETE') {
const prefix = route.path.replace(/:[^/]+$/, '');
sections.push(` (let ((id (path-param req "${prefix}")))`);
sections.push(' (if id');
sections.push(' (begin');
sections.push(' ;; TODO: Delete resource');
sections.push(' (json-response req (hash ("status" "deleted"))))');
sections.push(' (json-error req "Not found" 404))))');
} else {
sections.push(` ;; TODO: Implement ${route.method} ${route.path}`);
sections.push(' (json-response req (hash ("status" "ok"))))');
}
sections.push('');
}
// Router
sections.push(';;; Router');
sections.push('(def (router req)');
sections.push(' (let ((method (http-request-method req))');
sections.push(' (path (http-request-path req)))');
sections.push(' (cond');
for (const route of routes) {
if (route.path.includes(':')) {
const prefix = route.path.replace(/:[^/]+$/, '');
sections.push(
` ((and (equal? method "${route.method}") (string-prefix? "${prefix}" path))`,
);
} else {
sections.push(
` ((and (equal? method "${route.method}") (equal? path "${route.path}"))`,
);
}
sections.push(` (${route.handler} req))`);
}
sections.push(' (else');
sections.push(' (json-error req "Not found" 404)))))');
sections.push('');
// Main entry point
sections.push(';;; Main entry point');
sections.push('(def (main . args)');
sections.push(` (def port ${serverPort})`);
sections.push(' (displayln (format "Starting server on port ~a" port))');
sections.push(' (let ((httpd (start-http-server! (format "0.0.0.0:~a" port)');
sections.push(' handler: router)))');
sections.push(' (displayln "Server started. Press Ctrl+C to stop.")');
sections.push(' (thread-join! httpd)))');
const code = sections.join('\n');
const output = [
`## HTTP Server Scaffold: ${modName}`,
'',
`Routes: ${routes.length}`,
`Port: ${serverPort}`,
'',
'```scheme',
code,
'```',
'',
'### Usage',
'1. Save to a `.ss` file',
'2. Build with `gerbil build`',
'3. Run the server',
'',
'**Note**: Verify httpd imports with `gerbil_module_exports :std/net/httpd` — ',
'the API may vary between Gerbil versions.',
];
return {
content: [{ type: 'text' as const, text: output.join('\n') }],
};
},
);
}