From 1e254e32fd1066925bd19901d90c42a215b0bb16 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Fri, 27 Jun 2025 12:56:38 +0100 Subject: [PATCH] feat: Implement MCP Hello World Server with basic tools and configuration --- .gitignore | 6 +- MCP/.env.example | 12 +++ MCP/Dockerfile.tpl | 53 ++++++++++ MCP/Index.ts | 190 +++++++++++++++++++++++++++++++++++ MCP/README.md | 92 +++++++++++++++++ MCP/__tests__/server.test.ts | 16 +++ MCP/build/.gitkeep | 2 + MCP/jest.config.json | 13 +++ MCP/nodemon.json | 6 ++ MCP/package.json | 32 ++++++ MCP/tsconfig.json | 30 ++++++ 11 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 MCP/.env.example create mode 100644 MCP/Dockerfile.tpl create mode 100644 MCP/Index.ts create mode 100644 MCP/README.md create mode 100644 MCP/__tests__/server.test.ts create mode 100644 MCP/build/.gitkeep create mode 100644 MCP/jest.config.json create mode 100644 MCP/nodemon.json create mode 100644 MCP/package.json create mode 100644 MCP/tsconfig.json diff --git a/.gitignore b/.gitignore index 60c01a18bb..92737f6af1 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,8 @@ Terraform/** TerraformTest/** -terraform-provider-example/** +terraform-provider-example/** + +# MCP Hello World Server +mcp-hello-world/build/ +mcp-hello-world/.env diff --git a/MCP/.env.example b/MCP/.env.example new file mode 100644 index 0000000000..0fcd748946 --- /dev/null +++ b/MCP/.env.example @@ -0,0 +1,12 @@ +# Environment variables for OneUptime MCP Hello World Server + +# Logging +LOG_LEVEL=info + +# Server Configuration +NODE_ENV=development +PORT=3002 + +# OneUptime Configuration +APP_NAME=mcp-hello-world +APP_VERSION=1.0.0 diff --git a/MCP/Dockerfile.tpl b/MCP/Dockerfile.tpl new file mode 100644 index 0000000000..bfa1fd8eb3 --- /dev/null +++ b/MCP/Dockerfile.tpl @@ -0,0 +1,53 @@ +# +# OneUptime MCP Hello World Dockerfile +# + +# Pull base image nodejs image. +FROM public.ecr.aws/docker/library/node:22.3.0 +RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global + +RUN npm config set fetch-retries 5 +RUN npm config set fetch-retry-mintimeout 100000 +RUN npm config set fetch-retry-maxtimeout 600000 + +ARG GIT_SHA +ARG APP_VERSION + +ENV GIT_SHA=${GIT_SHA} +ENV APP_VERSION=${APP_VERSION} + +# IF APP_VERSION is not set, set it to 1.0.0 +RUN if [ -z "$APP_VERSION" ]; then export APP_VERSION=1.0.0; fi + +# Install bash. +RUN apt-get install bash -y && apt-get install curl -y + +# Install python +RUN apt-get update && apt-get install -y .gyp python3 make g++ + +#Use bash shell by default +SHELL ["/bin/bash", "-c"] +RUN npm install typescript -g + +USER root + +RUN mkdir /usr/src + +WORKDIR /usr/src/Common +COPY ./Common/package*.json /usr/src/Common/ +RUN npm install +COPY ./Common /usr/src/Common + +WORKDIR /usr/src/app + +# Install app dependencies +COPY ./mcp-hello-world/package*.json /usr/src/app/ +RUN npm install --only=production +COPY ./mcp-hello-world /usr/src/app + +# Expose Port +EXPOSE 3002 + +#Run the app +RUN npm run compile +CMD [ "npm", "start" ] diff --git a/MCP/Index.ts b/MCP/Index.ts new file mode 100644 index 0000000000..2021a198bf --- /dev/null +++ b/MCP/Index.ts @@ -0,0 +1,190 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ErrorCode, + ListToolsRequestSchema, + McpError, +} from "@modelcontextprotocol/sdk/types.js"; +import logger from "Common/Server/Utils/Logger"; +import dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +const APP_NAME: string = "mcp-hello-world"; + +logger.info("OneUptime Hello World MCP Server is starting..."); + +class HelloWorldMCPServer { + private server: Server; + + constructor() { + this.server = new Server( + { + name: "oneuptime-mcp", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupHandlers(); + } + + private setupHandlers(): void { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "hello", + description: "Say hello with a personalized greeting", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "Name of the person to greet", + }, + }, + required: ["name"], + }, + }, + { + name: "get_time", + description: "Get the current server time", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "echo", + description: "Echo back any message", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "Message to echo back", + }, + }, + required: ["message"], + }, + }, + ], + }; + }); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case "hello": { + const personName = args?.name as string; + if (!personName) { + throw new McpError( + ErrorCode.InvalidParams, + "Name parameter is required" + ); + } + + logger.info(`Saying hello to: ${personName}`); + return { + content: [ + { + type: "text", + text: `Hello, ${personName}! Welcome to OneUptime's Hello World MCP Server! 🚀`, + }, + ], + }; + } + + case "get_time": { + const currentTime = new Date().toISOString(); + logger.info(`Returning current time: ${currentTime}`); + return { + content: [ + { + type: "text", + text: `Current server time: ${currentTime}`, + }, + ], + }; + } + + case "echo": { + const message = args?.message as string; + if (!message) { + throw new McpError( + ErrorCode.InvalidParams, + "Message parameter is required" + ); + } + + logger.info(`Echoing message: ${message}`); + return { + content: [ + { + type: "text", + text: `Echo: ${message}`, + }, + ], + }; + } + + default: + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${name}` + ); + } + } catch (error) { + logger.error(`Error executing tool ${name}:`, error); + throw error; + } + }); + } + + async run(): Promise { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + + logger.info("OneUptime Hello World MCP Server is running!"); + logger.info("Available tools: hello, get_time, echo"); + } +} + +// Start the server +async function main(): Promise { + try { + const mcpServer = new HelloWorldMCPServer(); + await mcpServer.run(); + } catch (error) { + logger.error("Failed to start MCP server:", error); + process.exit(1); + } +} + +// Handle graceful shutdown +process.on("SIGINT", () => { + logger.info("Received SIGINT, shutting down gracefully..."); + process.exit(0); +}); + +process.on("SIGTERM", () => { + logger.info("Received SIGTERM, shutting down gracefully..."); + process.exit(0); +}); + +// Start the server +main().catch((error) => { + logger.error("Unhandled error:", error); + process.exit(1); +}); diff --git a/MCP/README.md b/MCP/README.md new file mode 100644 index 0000000000..619f0b32cc --- /dev/null +++ b/MCP/README.md @@ -0,0 +1,92 @@ +# OneUptime Hello World MCP Server + +A basic Hello World implementation of a Model Context Protocol (MCP) server for OneUptime. + +## What is this? + +This is a simple MCP server that demonstrates how to create a Model Context Protocol server within the OneUptime ecosystem. It provides basic tools that can be used by AI assistants like Claude to interact with the server. + +## Available Tools + +1. **hello** - Say hello with a personalized greeting + - Parameters: `name` (string, required) + - Example: Returns "Hello, [name]! Welcome to OneUptime's Hello World MCP Server! 🚀" + +2. **get_time** - Get the current server time + - Parameters: None + - Example: Returns current ISO timestamp + +3. **echo** - Echo back any message + - Parameters: `message` (string, required) + - Example: Returns "Echo: [your message]" + +## Development + +### Prerequisites + +- Node.js 18+ +- npm or yarn +- TypeScript + +### Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Start development server: + ```bash + npm run dev + ``` + +3. Start production server: + ```bash + npm start + ``` + +### Docker + +Build and run with Docker: + +```bash +# Build the Docker image +docker build -f Dockerfile.tpl -t oneuptime-mcp-hello-world . + +# Run the container +docker run -it oneuptime-mcp-hello-world +``` + +## Usage + +This MCP server communicates over stdio and is designed to be used with MCP-compatible clients like Claude Desktop or other AI assistants that support the Model Context Protocol. + +### Example Configuration for Claude Desktop + +Add this to your Claude Desktop MCP settings: + +```json +{ + "mcpServers": { + "oneuptime": { + "command": "node", + "args": ["--require", "ts-node/register", "/path/to/mcp-hello-world/Index.ts"] + } + } +} +``` + +## Architecture + +The server is built using: +- **@modelcontextprotocol/sdk**: Official MCP SDK for TypeScript +- **OneUptime Common**: Shared utilities and logging from OneUptime +- **TypeScript**: For type safety and better development experience + +## Contributing + +This is part of the OneUptime project. Follow the standard OneUptime development practices and coding standards. + +## License + +Apache-2.0 - see the OneUptime project license for details. diff --git a/MCP/__tests__/server.test.ts b/MCP/__tests__/server.test.ts new file mode 100644 index 0000000000..4e7c3f3515 --- /dev/null +++ b/MCP/__tests__/server.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from '@jest/globals'; + +describe('MCP Hello World Server', () => { + it('should have basic structure', () => { + // Basic test to ensure the test setup works + expect(true).toBe(true); + }); + + it('should export required tools', () => { + // Test for tool definitions + const expectedTools = ['hello', 'get_time', 'echo']; + expect(expectedTools).toContain('hello'); + expect(expectedTools).toContain('get_time'); + expect(expectedTools).toContain('echo'); + }); +}); diff --git a/MCP/build/.gitkeep b/MCP/build/.gitkeep new file mode 100644 index 0000000000..46ccf0d0f9 --- /dev/null +++ b/MCP/build/.gitkeep @@ -0,0 +1,2 @@ +# Build directory for compiled TypeScript files +# This directory will contain the compiled JavaScript output diff --git a/MCP/jest.config.json b/MCP/jest.config.json new file mode 100644 index 0000000000..d048d97372 --- /dev/null +++ b/MCP/jest.config.json @@ -0,0 +1,13 @@ +{ + "preset": "ts-jest", + "testEnvironment": "node", + "testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + "collectCoverageFrom": [ + "**/*.ts", + "!**/*.d.ts", + "!**/node_modules/**", + "!**/build/**" + ], + "setupFilesAfterEnv": [], + "testTimeout": 30000 +} diff --git a/MCP/nodemon.json b/MCP/nodemon.json new file mode 100644 index 0000000000..fbf637fcca --- /dev/null +++ b/MCP/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["Index.ts"], + "ext": "ts", + "ignore": ["build/*"], + "exec": "npm start" +} diff --git a/MCP/package.json b/MCP/package.json new file mode 100644 index 0000000000..c9db995c7d --- /dev/null +++ b/MCP/package.json @@ -0,0 +1,32 @@ +{ + "name": "@oneuptime/mcp-hello-world", + "version": "1.0.0", + "description": "OneUptime Hello World MCP Server", + "main": "Index.ts", + "type": "module", + "scripts": { + "start": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts", + "compile": "tsc", + "clear-modules": "rm -rf node_modules && rm package-lock.json && npm install", + "dev": "npx nodemon", + "audit": "npm audit --audit-level=low", + "dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true", + "test": "jest --passWithNoTests" + }, + "author": "OneUptime (https://oneuptime.com/)", + "license": "Apache-2.0", + "dependencies": { + "Common": "file:./Common", + "@modelcontextprotocol/sdk": "^0.6.0", + "ts-node": "^10.9.1", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/jest": "^27.5.0", + "@types/node": "^17.0.31", + "jest": "^28.1.0", + "nodemon": "^2.0.20", + "ts-jest": "^28.0.2", + "typescript": "^5.8.3" + } +} diff --git a/MCP/tsconfig.json b/MCP/tsconfig.json new file mode 100644 index 0000000000..003f073078 --- /dev/null +++ b/MCP/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es6"], + "allowJs": true, + "outDir": "./build", + "rootDir": "./", + "strict": true, + "moduleResolution": "node", + "baseUrl": "./", + "paths": { + "Common/*": ["./Common/*"] + }, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "build" + ] +}