#!/usr/bin/env node /** * Example FastMCP server demonstrating custom HTTP routes using Hono's native API. * * This example shows how to: * - Use getApp() to access Hono's native API * - Add REST API endpoints with Hono's standard methods (app.get(), app.post(), etc.) * - Implement authentication with a custom helper function * - Handle file uploads * - Serve admin interfaces * - Create webhooks * - OAuth discovery endpoints * - Integrate custom routes with MCP tools * * Run with: * npx fastmcp dev src/examples/custom-routes.ts * npx fastmcp inspect src/examples/custom-routes.ts * * Or directly: * node dist/examples/custom-routes.js --transport=http-stream --port=8080 */ import type { Context } from "hono"; import { z } from "zod"; import { FastMCP } from "../FastMCP.js"; // Example in-memory data store interface User { email: string; id: string; name: string; } const users = new Map([ ["1", { email: "alice@example.com", id: "1", name: "Alice" }], ["2", { email: "bob@example.com", id: "2", name: "Bob" }], ]); let requestCount = 0; // Simple authentication for demonstration interface UserAuth { [key: string]: unknown; role: string; userId: string; } // Create the FastMCP server with authentication const server = new FastMCP({ // Simple authentication - in production, use proper tokens/JWTs authenticate: async (req) => { const authHeader = req.headers.authorization; if (authHeader === "Bearer admin-token") { return { role: "admin", userId: "admin" }; } else if (authHeader === "Bearer user-token") { return { role: "user", userId: "user1" }; } throw new Error("Invalid or missing authentication"); }, name: "custom-routes-example", version: "1.0.0", }); // Get the Hono app instance for direct access to Hono's native API const app = server.getApp(); // Helper to get authentication from Node.js request const getAuth = async (c: Context): Promise => { const req = c.env.incoming; const authHeader = req.headers.authorization; if (authHeader === "Bearer admin-token") { return { role: "admin", userId: "admin" }; } else if (authHeader === "Bearer user-token") { return { role: "user", userId: "user1" }; } return null; }; // ===== PUBLIC ROUTES (No Authentication Required) ===== // OAuth discovery endpoint - public by design app.get("/.well-known/openid-configuration", async (c) => { return c.json({ authorization_endpoint: "https://example.com/oauth/authorize", issuer: "https://example.com", jwks_uri: "https://example.com/.well-known/jwks.json", response_types_supported: ["code"], scopes_supported: ["openid", "profile", "email"], subject_types_supported: ["public"], token_endpoint: "https://example.com/oauth/token", }); }); // OAuth protected resource metadata - also public app.get("/.well-known/oauth-protected-resource", async (c) => { return c.json({ authorizationServers: ["https://example.com"], resource: "https://example.com/api", scopesSupported: ["read", "write"], }); }); // Public status endpoint - no auth needed app.get("/status", async (c) => { return c.json({ message: "Server is running", status: "healthy", timestamp: new Date().toISOString(), version: "1.0.0", }); }); // Public documentation endpoint app.get("/docs", async (c) => { const html = ` API Documentation

Custom Routes API Documentation

Public Endpoints (No Authentication)

GET /status
✓ Public - Server health status
GET /.well-known/openid-configuration
✓ Public - OAuth discovery

Private Endpoints (Authentication Required)

GET /api/users
🔒 Requires: Bearer token
GET /admin
🔒 Requires: admin token

Authentication

Use one of these tokens in the Authorization header:

  • Bearer admin-token - Admin access
  • Bearer user-token - User access

Examples

# Public endpoint (no auth needed)
curl http://localhost:8080/status

# Private endpoint (auth required)
curl -H "Authorization: Bearer user-token" http://localhost:8080/api/users
      
`; return c.html(html); }); // Public static assets app.get("/public/*", async (c) => { // In a real app, you'd serve actual files here return c.json({ file: c.req.url, message: "This would serve static files", public: true, }); }); // ===== PRIVATE ROUTES (Authentication Required) ===== // Add custom routes for a REST API app.get("/api/users", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const userList = Array.from(users.values()); return c.json({ authenticated_as: auth.userId, count: userList.length, role: auth.role, users: userList, }); }); app.get("/api/users/:id", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const id = c.req.param("id"); const user = users.get(id); if (!user) { return c.json({ error: "User not found" }, 404); } return c.json(user); }); app.post("/api/users", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const body = (await c.req.json()) as { email: string; name: string }; if (!body.name || !body.email) { return c.json({ error: "Name and email are required" }, 400); } const id = String(users.size + 1); const newUser: User = { email: body.email, id, name: body.name, }; users.set(id, newUser); return c.json(newUser, 201); }); app.put("/api/users/:id", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const id = c.req.param("id"); const user = users.get(id); if (!user) { return c.json({ error: "User not found" }, 404); } const body = (await c.req.json()) as Partial; const updatedUser = { ...user, ...body, id: user.id }; users.set(user.id, updatedUser); return c.json(updatedUser); }); app.delete("/api/users/:id", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const id = c.req.param("id"); if (!users.has(id)) { return c.json({ error: "User not found" }, 404); } users.delete(id); return c.body(null, 204); }); // Add a simple admin dashboard - requires admin role app.get("/admin", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } // Check for admin role if (auth.role !== "admin") { return c.json({ error: "Admin access required" }, 403); } const html = ` Admin Dashboard

Admin Dashboard

Total Users: ${users.size}
Request Count: ${requestCount}
Server Time: ${new Date().toISOString()}

Users

    ${Array.from(users.values()) .map((u) => `
  • ${u.name} (${u.email})
  • `) .join("")}
`; return c.html(html); }); // Add a webhook endpoint app.post("/webhook/github", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } const payload = await c.req.json(); const event = c.req.header("x-github-event"); console.log(`GitHub webhook received: ${event}`, payload); // Process webhook (e.g., trigger MCP tools) return c.json({ event, received: true }); }); // Add a file upload endpoint app.post("/upload", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } try { const body = await c.req.text(); const size = Buffer.byteLength(body); return c.json({ message: "File received", size: `${size} bytes`, }); } catch (error) { return c.json( { error: error instanceof Error ? error.message : "Upload failed", }, 500, ); } }); // Add middleware-like request counting app.get("/stats", async (c) => { const auth = await getAuth(c); if (!auth) { return c.json({ error: "Authentication required" }, 401); } requestCount++; return c.json({ requests: requestCount, timestamp: Date.now(), uptime: process.uptime(), }); }); // Add MCP tools that can interact with the custom routes server.addTool({ description: "List all users from the REST API", execute: async () => { const userList = Array.from(users.values()); return { content: [ { text: `Found ${userList.length} users:\n${userList .map((u) => `- ${u.name} (${u.email})`) .join("\n")}`, type: "text", }, ], }; }, name: "list_users", parameters: z.object({}), }); server.addTool({ description: "Create a new user via the REST API", execute: async ({ email, name }) => { const id = String(users.size + 1); const newUser: User = { email, id, name }; users.set(id, newUser); return { content: [ { text: `User created successfully:\nID: ${id}\nName: ${name}\nEmail: ${email}`, type: "text", }, ], }; }, name: "create_user", parameters: z.object({ email: z.string().email(), name: z.string(), }), }); server.addTool({ description: "Get server statistics", execute: async () => { return { content: [ { text: `Server Statistics: - Total Users: ${users.size} - Request Count: ${requestCount} - Uptime: ${Math.floor(process.uptime())} seconds - Memory Usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)} MB`, type: "text", }, ], }; }, name: "get_stats", parameters: z.object({}), }); // Add a resource that exposes the user list server.addResource({ description: "Current user database", load: async () => ({ text: JSON.stringify(Array.from(users.values()), null, 2), }), mimeType: "application/json", name: "user-database", uri: "resource://users", }); // Start the server const PORT = process.env.FASTMCP_PORT ? parseInt(process.env.FASTMCP_PORT) : 8080; server .start({ httpStream: { port: PORT }, transportType: "httpStream", }) .then(() => { console.log(` 🚀 Custom Routes Example Server Started! MCP Endpoint: http://localhost:${PORT}/mcp Health Check: http://localhost:${PORT}/health PUBLIC ROUTES (No Authentication): - Status: http://localhost:${PORT}/status - Docs: http://localhost:${PORT}/docs - OAuth Config: http://localhost:${PORT}/.well-known/openid-configuration - Static Files: http://localhost:${PORT}/public/* PRIVATE ROUTES (Authentication Required): - REST API: http://localhost:${PORT}/api/users - Admin Panel: http://localhost:${PORT}/admin (admin only) - Statistics: http://localhost:${PORT}/stats - File Upload: http://localhost:${PORT}/upload - GitHub Hook: http://localhost:${PORT}/webhook/github Authentication: Use "Authorization: Bearer admin-token" or "Bearer user-token" Try these commands: # Public routes (no auth needed) curl http://localhost:${PORT}/status curl http://localhost:${PORT}/docs curl http://localhost:${PORT}/.well-known/openid-configuration # Private routes (auth required) curl -H "Authorization: Bearer user-token" http://localhost:${PORT}/api/users curl -H "Authorization: Bearer admin-token" http://localhost:${PORT}/admin curl -X POST -H "Authorization: Bearer user-token" -H "Content-Type: application/json" \\ -d '{"name":"Charlie","email":"charlie@example.com"}' \\ http://localhost:${PORT}/api/users # Test authentication failure curl http://localhost:${PORT}/api/users # Should return 401 MCP Tools available: - list_users - create_user - get_stats Test with MCP Inspector: npx fastmcp inspect src/examples/custom-routes.ts `); }) .catch((error) => { console.error("Failed to start server:", error); process.exit(1); });