diff --git a/README.md b/README.md index 064d4d2..4eb92ee 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6) -![MCP](https://img.shields.io/badge/MCP-1.7+-green) -A CLI tool to quickly get started building your very own MCP (Model Context Protocol) server. +A CLI tool to quickly get started building your very own MCP (Model Context Protocol) server using FastMCP ## 📋 Usage @@ -13,23 +12,23 @@ A CLI tool to quickly get started building your very own MCP (Model Context Prot npx @mcpdotdirect/create-mcp-server # Or with npm -npm init @mcpdotdirect/create-mcp-server +npm init @mcpdotdirect/mcp-server ``` ## 🔭 What's Included The template includes: -- Basic server setup with both stdio and HTTP transport options +- Basic server setup with both stdio and HTTP transport options using FastMCP - Structure for defining MCP tools, resources, and prompts - TypeScript configuration - Development scripts and configuration ## ✨ Features +- **FastMCP**: Built using the FastMCP framework for simpler implementation - **Dual Transport Support**: Run your MCP server over stdio or HTTP - **TypeScript**: Full TypeScript support for type safety -- **MCP SDK**: Built on the official Model Context Protocol SDK - **Extensible**: Easy to add custom tools, resources, and prompts ## 🚀 Getting Started @@ -71,41 +70,214 @@ After creating your project: > **Note**: The default scripts in package.json use Bun as the runtime (e.g., `bun run src/index.ts`). If you prefer to use a different package manager or runtime, you can modify these scripts in your package.json file to use Node.js or another runtime of your choice. +## 📖 Detailed Usage + +### Transport Methods + +The MCP server supports two transport methods: + +1. **stdio Transport** (Command Line Mode): + - Runs on your **local machine** + - Managed automatically by Cursor + - Communicates directly via `stdout` + - Only accessible by you locally + - Ideal for personal development and tools + +2. **SSE Transport** (HTTP Web Mode): + - Can run **locally or remotely** + - Managed and run by you + - Communicates **over the network** + - Can be **shared** across machines + - Ideal for team collaboration and shared tools + +### Running the Server Locally + +#### stdio Transport (CLI Mode) + +Start the server in stdio mode for CLI tools: + +```bash +# Start the stdio server +npm start +# or with other package managers +yarn start +pnpm start +bun start + +# Start the server in development mode with auto-reload +npm run dev +# or +yarn dev +pnpm dev +bun dev +``` + +#### HTTP Transport (Web Mode) + +Start the server in HTTP mode for web applications: + +```bash +# Start the HTTP server +npm run start:http +# or +yarn start:http +pnpm start:http +bun start:http + +# Start the HTTP server in development mode with auto-reload +npm run dev:http +# or +yarn dev:http +pnpm dev:http +bun dev:http +``` + +By default, the HTTP server runs on port 3001. You can change this by setting the PORT environment variable: + +```bash +# Start the HTTP server on a custom port +PORT=8080 npm run start:http +``` + +### Connecting to the Server + +#### Connecting from Cursor + +To connect to your MCP server from Cursor: + +1. Open Cursor and go to Settings (gear icon in the bottom left) +2. Click on "Features" in the left sidebar +3. Scroll down to "MCP Servers" section +4. Click "Add new MCP server" +5. Enter the following details: + - Server name: `my-mcp-server` (or any name you prefer) + - For stdio mode: + - Type: `command` + - Command: The path to your server executable, e.g., `npm start` + - For SSE mode: + - Type: `url` + - URL: `http://localhost:3001/sse` +6. Click "Save" + +#### Using mcp.json with Cursor + +For a more portable configuration, create an `.cursor/mcp.json` file in your project's root directory: + +```json +{ + "mcpServers": { + "my-mcp-stdio": { + "command": "npm", + "args": [ + "start" + ], + "env": { + "NODE_ENV": "development" + } + }, + "my-mcp-sse": { + "url": "http://localhost:3001/sse" + } + } +} +``` + +You can also create a global configuration at `~/.cursor/mcp.json` to make your MCP servers available in all your Cursor workspaces. + +Note: +- The `command` type entries run the server in stdio mode +- The `url` type entry connects to the HTTP server using SSE transport +- You can provide environment variables using the `env` field +- When connecting via SSE with FastMCP, use the full URL including the `/sse` path: `http://localhost:3001/sse` + +### Testing Your Server with CLI Tools + +FastMCP provides built-in tools for testing your server: + +```bash +# Test with mcp-cli +npx fastmcp dev server.js + +# Inspect with MCP Inspector +npx fastmcp inspect server.ts +``` + +### Using Environment Variables + +You can customize the server using environment variables: + +```bash +# Change the HTTP port (default is 3001) +PORT=8080 npm run start:http + +# Change the host binding (default is 0.0.0.0) +HOST=127.0.0.1 npm run start:http +``` + ## 🛠️ Adding Custom Tools and Resources -When adding custom tools, resources, or prompts to your MCP server: - -1. Use underscores (`_`) instead of hyphens (`-`) in all resource, tool, and prompt names - ```typescript - // Good: Uses underscores - server.tool( - "my_custom_tool", - "Description of my custom tool", - { - param_name: z.string().describe("Parameter description") - }, - async (params) => { - // Tool implementation - } - ); - - // Bad: Uses hyphens, may cause issues with Cursor - server.tool( - "my-custom-tool", - "Description of my custom tool", - { - param-name: z.string().describe("Parameter description") - }, - async (params) => { - // Tool implementation - } - ); - ``` +When adding custom tools, resources, or prompts to your FastMCP server: + +### Tools + +```typescript +server.addTool({ + name: "hello_world", + description: "A simple hello world tool", + parameters: z.object({ + name: z.string().describe("Name to greet") + }), + execute: async (params) => { + return `Hello, ${params.name}!`; + } +}); +``` -2. This naming convention ensures compatibility with Cursor and other AI tools that interact with your MCP server +### Resources + +```typescript +server.addResourceTemplate({ + uriTemplate: "example://{id}", + name: "Example Resource", + mimeType: "text/plain", + arguments: [ + { + name: "id", + description: "Resource ID", + required: true, + }, + ], + async load({ id }) { + return { + text: `This is an example resource with ID: ${id}` + }; + } +}); +``` + +### Prompts + +```typescript +server.addPrompt({ + name: "greeting", + description: "A simple greeting prompt", + arguments: [ + { + name: "name", + description: "Name to greet", + required: true, + }, + ], + load: async ({ name }) => { + return `Hello, ${name}! How can I help you today?`; + } +}); +``` ## 📚 Documentation +For more information about FastMCP, visit [FastMCP GitHub Repository](https://github.com/punkpeye/fastmcp). + For more information about the Model Context Protocol, visit the [MCP Documentation](https://modelcontextprotocol.io/introduction). ## 📄 License diff --git a/bin/create-mcp-server.js b/bin/create-mcp-server.js index e1a3c49..09021b0 100755 --- a/bin/create-mcp-server.js +++ b/bin/create-mcp-server.js @@ -3,7 +3,6 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { execSync } from 'child_process'; // Get the directory where the source files are stored const __filename = fileURLToPath(import.meta.url); @@ -171,7 +170,7 @@ function createProjectPackageJson() { "typescript": "^5.8.2" }, dependencies: { - "@modelcontextprotocol/sdk": "^1.7.0", + "fastmcp": "^1.21.0", "cors": "^2.8.5", "express": "^4.21.2", "zod": "^3.24.2" diff --git a/package.json b/package.json index cd00430..bb95865 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,9 @@ "typescript": "^5.8.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.7.0", "cors": "^2.8.5", "express": "^4.21.2", + "fastmcp": "^1.21.0", "zod": "^3.24.2" }, "engines": { diff --git a/src/core/prompts.ts b/src/core/prompts.ts index 6dc418c..1478b08 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -1,26 +1,24 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FastMCP } from "fastmcp"; import { z } from "zod"; /** * Register all prompts with the MCP server - * @param server The MCP server instance + * @param server The FastMCP server instance */ -export function registerPrompts(server: McpServer) { +export function registerPrompts(server: FastMCP) { // Example prompt - server.prompt( - "greeting", - "A simple greeting prompt", - { - name: z.string().describe("Name to greet") - }, - (params: { name: string }) => ({ - messages: [{ - role: "user", - content: { - type: "text", - text: `Hello, ${params.name}! How can I help you today?` - } - }] - }) - ); + server.addPrompt({ + name: "greeting", + description: "A simple greeting prompt", + arguments: [ + { + name: "name", + description: "Name to greet", + required: true, + }, + ], + load: async ({ name }) => { + return `Hello, ${name}! How can I help you today?`; + } + }); } diff --git a/src/core/resources.ts b/src/core/resources.ts index 2f23a96..1820a33 100644 --- a/src/core/resources.ts +++ b/src/core/resources.ts @@ -1,23 +1,27 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FastMCP } from "fastmcp"; import * as services from "./services/index.js"; /** * Register all resources with the MCP server - * @param server The MCP server instance + * @param server The FastMCP server instance */ -export function registerResources(server: McpServer) { +export function registerResources(server: FastMCP) { // Example resource - server.resource( - "example_resource", - "example://{id}", - async (uri: URL) => { - const id = uri.pathname.split('/').pop(); + server.addResourceTemplate({ + uriTemplate: "example://{id}", + name: "Example Resource", + mimeType: "text/plain", + arguments: [ + { + name: "id", + description: "Resource ID", + required: true, + }, + ], + async load({ id }) { return { - contents: [{ - uri: uri.toString(), - text: `This is an example resource with ID: ${id}` - }] + text: `This is an example resource with ID: ${id}` }; } - ); + }); } \ No newline at end of file diff --git a/src/core/tools.ts b/src/core/tools.ts index 0b63c09..728c0e8 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -1,50 +1,36 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FastMCP } from "fastmcp"; import { z } from "zod"; import * as services from "./services/index.js"; /** * Register all tools with the MCP server * - * @param server The MCP server instance + * @param server The FastMCP server instance */ -export function registerTools(server: McpServer) { +export function registerTools(server: FastMCP) { // Greeting tool - server.tool( - "hello_world", - "A simple hello world tool", - { + server.addTool({ + name: "hello_world", + description: "A simple hello world tool", + parameters: z.object({ name: z.string().describe("Name to greet") - }, - async (params: { name: string }) => { + }), + execute: async (params) => { const greeting = services.GreetingService.generateGreeting(params.name); - return { - content: [ - { - type: "text", - text: greeting - } - ] - }; + return greeting; } - ); + }); // Farewell tool - server.tool( - "goodbye", - "A simple goodbye tool", - { + server.addTool({ + name: "goodbye", + description: "A simple goodbye tool", + parameters: z.object({ name: z.string().describe("Name to bid farewell to") - }, - async (params: { name: string }) => { + }), + execute: async (params) => { const farewell = services.GreetingService.generateFarewell(params.name); - return { - content: [ - { - type: "text", - text: farewell - } - ] - }; + return farewell; } - ); + }); } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ee02c86..2f94807 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,15 @@ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { FastMCP } from "fastmcp"; import startServer from "./server/server.js"; // Start the server async function main() { try { const server = await startServer(); - const transport = new StdioServerTransport(); - await server.connect(transport); + + server.start({ + transportType: "stdio", + }); + console.error("MCP Server running on stdio"); } catch (error) { console.error("Error starting MCP server:", error); diff --git a/src/server/http-server.ts b/src/server/http-server.ts index c574e1e..d9ef213 100644 --- a/src/server/http-server.ts +++ b/src/server/http-server.ts @@ -1,201 +1,38 @@ -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import startServer from "./server.js"; -import express, { Request, Response } from "express"; -import cors from "cors"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -// Environment variables - hardcoded values -const PORT = 3001; -const HOST = '0.0.0.0'; +// Environment variables with default values +const PORT = parseInt(process.env.PORT || "3001", 10); -console.error(`Configured to listen on ${HOST}:${PORT}`); - -// Setup Express -const app = express(); -app.use(express.json()); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true, - exposedHeaders: ['Content-Type', 'Access-Control-Allow-Origin'] -})); - -// Add OPTIONS handling for preflight requests -app.options('*', cors()); - -// Keep track of active connections with session IDs -const connections = new Map(); - -// Initialize the server -let server: McpServer | null = null; -startServer().then(s => { - server = s; - console.error("MCP Server initialized successfully"); -}).catch(error => { - console.error("Failed to initialize server:", error); - process.exit(1); -}); - -// Define routes -// @ts-ignore -app.get("/sse", (req: Request, res: Response) => { - console.error(`Received SSE connection request from ${req.ip}`); - console.error(`Query parameters: ${JSON.stringify(req.query)}`); - - // Set CORS headers explicitly - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (!server) { - console.error("Server not initialized yet, rejecting SSE connection"); - return res.status(503).send("Server not initialized"); - } - - // Generate a unique session ID if one is not provided - // The sessionId is crucial for mapping SSE connections to message handlers - const sessionId = generateSessionId(); - console.error(`Creating SSE session with ID: ${sessionId}`); - - // Set SSE headers - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache, no-transform"); - res.setHeader("Connection", "keep-alive"); - - // Create transport - handle before writing to response +async function main() { try { - console.error(`Creating SSE transport for session: ${sessionId}`); - - // Create and store the transport keyed by session ID - // Note: The path must match what the client expects (typically "/messages") - const transport = new SSEServerTransport("/messages", res); - connections.set(sessionId, transport); + // Create and initialize the FastMCP server + const server = await startServer(); - // Handle connection close - req.on("close", () => { - console.error(`SSE connection closed for session: ${sessionId}`); - connections.delete(sessionId); + // Start the server with SSE transport + server.start({ + transportType: "sse", + sse: { + port: PORT, + endpoint: "/sse", + }, }); - // Connect transport to server - this must happen before sending any data - server.connect(transport).then(() => { - // Send an initial event with the session ID for the client to use in messages - // Only send this after the connection is established - console.error(`SSE connection established for session: ${sessionId}`); - - // Send the session ID to the client - res.write(`data: ${JSON.stringify({ type: "session_init", sessionId })}\n\n`); - }).catch((error: Error) => { - console.error(`Error connecting transport to server: ${error}`); - connections.delete(sessionId); - }); - } catch (error) { - console.error(`Error creating SSE transport: ${error}`); - connections.delete(sessionId); - res.status(500).send(`Internal server error: ${error}`); - } -}); - -// @ts-ignore -app.post("/messages", (req: Request, res: Response) => { - // Extract the session ID from the URL query parameters - let sessionId = req.query.sessionId?.toString(); - - // If no sessionId is provided and there's only one connection, use that - if (!sessionId && connections.size === 1) { - sessionId = Array.from(connections.keys())[0]; - console.error(`No sessionId provided, using the only active session: ${sessionId}`); - } - - console.error(`Received message for sessionId ${sessionId}`); - console.error(`Message body: ${JSON.stringify(req.body)}`); - - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (!server) { - console.error("Server not initialized yet"); - return res.status(503).json({ error: "Server not initialized" }); - } - - if (!sessionId) { - console.error("No session ID provided and multiple connections exist"); - return res.status(400).json({ - error: "No session ID provided. Please provide a sessionId query parameter or connect to /sse first.", - activeConnections: connections.size - }); - } - - const transport = connections.get(sessionId); - if (!transport) { - console.error(`Session not found: ${sessionId}`); - return res.status(404).json({ error: "Session not found" }); - } - - console.error(`Handling message for session: ${sessionId}`); - try { - transport.handlePostMessage(req, res).catch((error: Error) => { - console.error(`Error handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error.message}` }); - }); + console.error(`MCP Server running at http://localhost:${PORT}`); + console.error(`SSE endpoint: http://localhost:${PORT}/sse`); } catch (error) { - console.error(`Exception handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error}` }); + console.error("Failed to start server:", error); + process.exit(1); } -}); - -// Add a simple health check endpoint -app.get("/health", (req: Request, res: Response) => { - res.status(200).json({ - status: "ok", - server: server ? "initialized" : "initializing", - activeConnections: connections.size, - connectedSessionIds: Array.from(connections.keys()) - }); -}); - -// Add a root endpoint for basic info -app.get("/", (req: Request, res: Response) => { - res.status(200).json({ - name: "MCP Server", - version: "1.0.0", - endpoints: { - sse: "/sse", - messages: "/messages", - health: "/health" - }, - status: server ? "ready" : "initializing", - activeConnections: connections.size - }); -}); - -// Helper function to generate a UUID-like session ID -function generateSessionId(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); } // Handle process termination gracefully -process.on('SIGINT', () => { - console.error('Shutting down server...'); - connections.forEach((transport, sessionId) => { - console.error(`Closing connection for session: ${sessionId}`); - }); +process.on("SIGINT", () => { + console.error("Shutting down server..."); process.exit(0); }); -// Start the HTTP server on a different port (3001) to avoid conflicts -const httpServer = app.listen(PORT, HOST, () => { - console.error(`Template MCP Server running at http://${HOST}:${PORT}`); - console.error(`SSE endpoint: http://${HOST}:${PORT}/sse`); - console.error(`Messages endpoint: http://${HOST}:${PORT}/messages (sessionId optional if only one connection)`); - console.error(`Health check: http://${HOST}:${PORT}/health`); -}).on('error', (err: Error) => { - console.error(`Server error: ${err}`); +// Start the server +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); }); \ No newline at end of file diff --git a/src/server/server.ts b/src/server/server.ts index 1c0bf00..ec202c4 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,4 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FastMCP } from "fastmcp"; import { registerResources } from "../core/resources.js"; import { registerTools } from "../core/tools.js"; import { registerPrompts } from "../core/prompts.js"; @@ -6,8 +6,8 @@ import { registerPrompts } from "../core/prompts.js"; // Create and start the MCP server async function startServer() { try { - // Create a new MCP server instance - const server = new McpServer({ + // Create a new FastMCP server instance + const server = new FastMCP({ name: "MCP Server", version: "1.0.0" });