diff --git a/mcp-joke-server/README.md b/mcp-joke-server/README.md new file mode 100644 index 0000000..b2c3cc5 --- /dev/null +++ b/mcp-joke-server/README.md @@ -0,0 +1,27 @@ + +## About + +This project is a simple command-line tool that fetches a random joke from the Chuck Norris API. + +## Setup + +1. Clone this repository. +2. Install the necessary dependencies by running `npm install`. +3. Build the project by running `npm run build`. +4. Run the project with `npm start`. + +## Usage + +To get a joke, run the following command: + +```bash +node index.js +``` + +## Contributing + +Contributions are welcome. Please open an issue to discuss what you would like to change. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/mcp-joke-server/apprunner.yaml b/mcp-joke-server/apprunner.yaml new file mode 100644 index 0000000..2ccd84f --- /dev/null +++ b/mcp-joke-server/apprunner.yaml @@ -0,0 +1,11 @@ +version: 1.0 +runtime: nodejs18 +build: + commands: + build: + - npm install + - npm run build +run: + command: npm start + network: + port: 8080 \ No newline at end of file diff --git a/mcp-joke-server/package.json b/mcp-joke-server/package.json new file mode 100644 index 0000000..92bc164 --- /dev/null +++ b/mcp-joke-server/package.json @@ -0,0 +1,42 @@ +{ + "name": "mcp-get-joke", + "version": "0.1.3", + "description": "MCP server for getting joke", + "license": "MIT", + "keywords": [ + "modelcontextprotocol", + "mcp", + "mcp-server", + "get-joke" + ], + "author": "", + "type": "module", + "engines": { + "node": ">=18" + }, + "bin": { + "mcp-get-joke": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "watch": "tsc --watch", + "clean": "git clean -fdxn -e .env && read -p 'OK?' && git clean -fdx -e .env", + "do-publish": "npm run clean && npm install && npm publish --access=public", + "publish-dry-run": "npm run clean && npm install && npm publish --access=public --dry-run", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.3", + "express": "^5.1.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/express": "^5.0.1", + "@types/node": "^22", + "shx": "^0.3.4", + "typescript": "^5.7.2" + } +} diff --git a/mcp-joke-server/src/index.ts b/mcp-joke-server/src/index.ts new file mode 100644 index 0000000..84f499d --- /dev/null +++ b/mcp-joke-server/src/index.ts @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import express, { Request, Response } from "express"; + +const PORT_NUMBER = 8080; + +// Zod schema for joke tool (no input required) +const JokeArgumentsSchema = z.object({}); + +// Create MCP Server +const server = new Server( + { + name: "joke-server", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +// Define available tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "get-joke", + description: "Get a random programming joke", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, + }, + ], + }; +}); + +// Handle tool execution +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + if (name === "get-joke") { + JokeArgumentsSchema.parse(args); + + const jokeUrl = "https://official-joke-api.appspot.com/jokes/programming/random"; + const response = await fetch(jokeUrl); + const jokeData = await response.json(); + + const joke = jokeData?.[0]; + if (!joke) { + return { + content: [{ type: "text", text: "No joke found!" }], + }; + } + + return { + content: [ + { + type: "text", + text: `Here's a programming joke:\n\n${joke.setup}\n${joke.punchline}`, + }, + ], + }; + } else { + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error( + `Invalid arguments: ${error.errors + .map((e) => `${e.path.join(".")}: ${e.message}`) + .join(", ")}` + ); + } + throw error; + } +}); + +// Setup Express and SSE +const app = express(); +const transports: { [sessionId: string]: SSEServerTransport } = {}; +let messageHitCount = 0; + +app.get("/", (_: Request, res: Response) => { + res.send(` + + + + MCP Joke Server + + + +

MCP Joke Server

+

Server is running correctly!

+ +
Connection status will appear here...
+ + + + + `); +}); + +// Health check +app.get("/api/test", (_: Request, res: Response) => { + res.json({ status: "ok", message: "Joke server is working!" }); +}); + +// SSE handshake +app.get("/sse", async (req: Request, res: Response) => { + const transport = new SSEServerTransport("/messages", res); + transports[transport.sessionId] = transport; + res.on("close", () => { + delete transports[transport.sessionId]; + }); + await server.connect(transport); +}); + +app.post("/messages", async (req: Request, res: Response) => { + messageHitCount++; + const sessionId = req.query.sessionId as string; + const transport = transports[sessionId]; + if (transport) { + await transport.handlePostMessage(req, res); + } else { + res.status(400).send("No transport found for sessionId"); + } +}); + +app.get("/messages/hit-count", (_: Request, res: Response) => { + res.json({ hitCount: messageHitCount }); +}); + +// Start the server +async function main() { + console.log(`Joke MCP Server starting on http://localhost:${PORT_NUMBER}`); + app.listen(PORT_NUMBER, () => { + console.log(`Joke MCP Server running on http://localhost:${PORT_NUMBER}`); + }); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); diff --git a/mcp-joke-server/tsconfig.json b/mcp-joke-server/tsconfig.json new file mode 100644 index 0000000..786d3aa --- /dev/null +++ b/mcp-joke-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file