├── API.md ├── COMPONENTS.md ├── README.md ├── SEQUENCE_DIAGRAMS.md ├── TESTING.md ├── asconfig.json ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── src ├── wasm │ └── mcp-server.ts └── web │ ├── __tests__ │ ├── browser-transport.test.ts │ ├── integration.test.ts │ ├── server.test.ts │ ├── test-helpers.ts │ └── test-utils.ts │ ├── browser-transport.ts │ ├── index.html │ ├── main.ts │ └── server.ts ├── tsconfig.json └── vite.config.ts /API.md: -------------------------------------------------------------------------------- 1 | # WASM MCP Server API Documentation 2 | 3 | This document provides detailed information about the tools and resources available in the WASM MCP Server implementation. 4 | 5 | ## Tools 6 | 7 | ### Calculator Tool 8 | 9 | **Name:** `calculate` 10 | 11 | **Description:** 12 | Performs basic arithmetic operations on two numbers. 13 | 14 | **Schema:** 15 | ```typescript 16 | { 17 | operation: z.enum(["add", "subtract", "multiply", "divide"]), 18 | a: z.number(), 19 | b: z.number() 20 | } 21 | ``` 22 | 23 | **Parameters:** 24 | - `operation`: The arithmetic operation to perform 25 | - `"add"`: Addition (a + b) 26 | - `"subtract"`: Subtraction (a - b) 27 | - `"multiply"`: Multiplication (a * b) 28 | - `"divide"`: Division (a / b) 29 | - `a`: First number 30 | - `b`: Second number 31 | 32 | **Returns:** 33 | ```typescript 34 | { 35 | content: [{ 36 | type: "text", 37 | text: string // The result as a string 38 | }] 39 | } 40 | ``` 41 | 42 | **Errors:** 43 | - Division by zero when operation is "divide" and b is 0 44 | - Invalid operation type 45 | - Non-numeric inputs 46 | 47 | **Example:** 48 | ```typescript 49 | // Addition 50 | const result = await mcpServer.tools.callback('calculate', { 51 | operation: "add", 52 | a: 5, 53 | b: 3 54 | }); 55 | // Result: { content: [{ type: "text", text: "8" }] } 56 | 57 | // Division 58 | const result = await mcpServer.tools.callback('calculate', { 59 | operation: "divide", 60 | a: 10, 61 | b: 2 62 | }); 63 | // Result: { content: [{ type: "text", text: "5" }] } 64 | ``` 65 | 66 | ### Storage Tool 67 | 68 | **Name:** `set-storage` 69 | 70 | **Description:** 71 | Stores a value associated with a key in the server's memory. 72 | 73 | **Schema:** 74 | ```typescript 75 | { 76 | key: z.string(), 77 | value: z.string() 78 | } 79 | ``` 80 | 81 | **Parameters:** 82 | - `key`: String identifier for the stored value 83 | - `value`: String value to store 84 | 85 | **Returns:** 86 | ```typescript 87 | { 88 | content: [{ 89 | type: "text", 90 | text: string // Confirmation message 91 | }] 92 | } 93 | ``` 94 | 95 | **Example:** 96 | ```typescript 97 | const result = await mcpServer.tools.callback('set-storage', { 98 | key: "user-preference", 99 | value: "dark-mode" 100 | }); 101 | // Result: { content: [{ type: "text", text: "Stored value at key: user-preference" }] } 102 | ``` 103 | 104 | ## Resources 105 | 106 | ### Storage Resource 107 | 108 | **URI Template:** `storage://{key}` 109 | 110 | **Description:** 111 | Retrieves a value from storage using a key. 112 | 113 | **Parameters:** 114 | - `key`: The key to look up in storage (extracted from URI) 115 | 116 | **Returns:** 117 | ```typescript 118 | { 119 | contents: [{ 120 | uri: string, // The full URI used for the request 121 | text: string // The stored value or "Key not found" 122 | }] 123 | } 124 | ``` 125 | 126 | **Access Method:** 127 | ```typescript 128 | // Create URI object 129 | const uri = new URL(`storage://${key}`); 130 | 131 | // Call resource handler 132 | const result = await resourceHandler.readCallback(uri, { key }); 133 | ``` 134 | 135 | **Example:** 136 | ```typescript 137 | // Assuming we previously stored: { key: "theme", value: "dark" } 138 | const uri = new URL('storage://theme'); 139 | const result = await resourceHandler.readCallback(uri, { key: "theme" }); 140 | // Result: { contents: [{ uri: "storage://theme", text: "dark" }] } 141 | 142 | // For a non-existent key: 143 | const uri = new URL('storage://nonexistent'); 144 | const result = await resourceHandler.readCallback(uri, { key: "nonexistent" }); 145 | // Result: { contents: [{ uri: "storage://nonexistent", text: "Key not found" }] } 146 | ``` 147 | 148 | ## Implementation Notes 149 | 150 | ### Tool Registration 151 | Tools are registered with the MCP server using the `tool` method: 152 | ```typescript 153 | server.tool( 154 | name: string, 155 | schema: ZodObject, 156 | callback: (params: any) => Promise 157 | ) 158 | ``` 159 | 160 | ### Resource Registration 161 | Resources are registered using the `resource` method: 162 | ```typescript 163 | server.resource( 164 | name: string, 165 | template: string, 166 | callback: (uri: URL, params: any) => Promise 167 | ) 168 | ``` 169 | 170 | ### Error Handling 171 | All tools and resources should implement proper error handling: 172 | - Input validation using Zod schemas 173 | - Meaningful error messages 174 | - Proper type checking 175 | - Edge case handling (e.g., division by zero) 176 | 177 | ### Security Considerations 178 | - Input sanitization is handled by Zod schemas 179 | - No persistent storage between sessions 180 | - No sensitive data should be stored 181 | - All operations are synchronous and local to the browser 182 | 183 | ## Browser Transport Layer 184 | 185 | The browser transport layer handles communication between the client and the WASM MCP server: 186 | 187 | - Initialization occurs when the page loads 188 | - Message passing uses the browser's event system 189 | - Error handling for transport failures 190 | - State management for server connection 191 | 192 | For detailed implementation of the transport layer, refer to `browser-transport.ts`. 193 | -------------------------------------------------------------------------------- /COMPONENTS.md: -------------------------------------------------------------------------------- 1 | # WASM MCP Server Components Documentation 2 | 3 | This document provides detailed information about each major component in the WASM MCP Server implementation. 4 | 5 | ## Core Components 6 | 7 | ### 1. MCP Server (`server.ts`) 8 | 9 | **Purpose:** 10 | The core server implementation that handles tool registration, resource management, and request processing. 11 | 12 | **Key Responsibilities:** 13 | - Server initialization and configuration 14 | - Tool and resource registration 15 | - Schema validation 16 | - Request handling 17 | - Response formatting 18 | 19 | **Implementation Details:** 20 | ```typescript 21 | export function createServer(): McpServer { 22 | const server = new McpServer({ 23 | name: "WASM MCP Server", 24 | version: "1.0.0" 25 | }); 26 | // ... tool and resource registration 27 | } 28 | ``` 29 | 30 | **Key Features:** 31 | - Singleton server instance 32 | - Tool registration with schema validation 33 | - Resource template handling 34 | - In-memory storage management 35 | - Error handling and validation 36 | 37 | **Initialization Flow:** 38 | 1. Create server instance 39 | 2. Register calculator tool 40 | 3. Register storage resource 41 | 4. Register storage tool 42 | 5. Initialize internal storage 43 | 6. Return configured server 44 | 45 | ### 2. Client Integration (`main.ts`) 46 | 47 | **Purpose:** 48 | Handles client-side integration, UI interactions, and server communication. 49 | 50 | **Key Responsibilities:** 51 | - Server initialization in browser 52 | - UI event handling 53 | - Tool execution 54 | - Resource access 55 | - Error display 56 | - State management 57 | 58 | **Component Structure:** 59 | ``` 60 | - Environment Setup 61 | ├── Transport initialization 62 | ├── Server creation 63 | └── Connection establishment 64 | 65 | - Calculator Integration 66 | ├── UI initialization 67 | ├── Event handlers 68 | ├── Input validation 69 | └── Result display 70 | 71 | - Storage Integration 72 | ├── UI initialization 73 | ├── Set/Get handlers 74 | ├── Key/Value management 75 | └── Status display 76 | ``` 77 | 78 | **Implementation Flow:** 79 | ```typescript 80 | // 1. Environment Setup 81 | async function setupEnvironment() { 82 | const transport = new BrowserTransport(); 83 | await transport.start(); 84 | const server = createServer(); 85 | await server.connect(transport); 86 | return { server, transport }; 87 | } 88 | 89 | // 2. Calculator Integration 90 | async function initializeCalculator(transport: BrowserTransport) { 91 | // Set up event handlers for calculator UI 92 | // Handle calculations through transport 93 | // Display results/errors 94 | } 95 | 96 | // 3. Storage Integration 97 | async function initializeStorage(transport: BrowserTransport) { 98 | // Set up event handlers for storage UI 99 | // Handle set/get operations 100 | // Display results/errors 101 | } 102 | 103 | // 4. Main Initialization 104 | async function main() { 105 | const { transport } = await setupEnvironment(); 106 | await initializeCalculator(transport); 107 | await initializeStorage(transport); 108 | } 109 | ``` 110 | 111 | **Error Handling:** 112 | - Transport connection errors 113 | - Tool execution errors 114 | - Resource access errors 115 | - Input validation 116 | - UI element initialization 117 | - Message handling errors 118 | 119 | **State Management:** 120 | - Single transport instance shared across components 121 | - Isolated UI component initialization 122 | - Independent error handling per component 123 | - Asynchronous operation handling 124 | 125 | ### 3. Browser Transport (`browser-transport.ts`) 126 | 127 | **Purpose:** 128 | Provides communication layer between the client and WASM server. 129 | 130 | **Key Responsibilities:** 131 | - Message passing 132 | - Event handling 133 | - Connection management 134 | - Error handling 135 | 136 | **Implementation Details:** 137 | ```typescript 138 | export class BrowserTransport { 139 | private connected: boolean = false; 140 | 141 | async start() { 142 | // Initialize transport 143 | } 144 | 145 | async send(message: any) { 146 | // Send message to server 147 | } 148 | 149 | onMessage(callback: (message: any) => void) { 150 | // Handle incoming messages 151 | } 152 | } 153 | ``` 154 | 155 | **Message Flow:** 156 | 1. Client initiates request 157 | 2. Transport serializes message 158 | 3. WASM server processes request 159 | 4. Response returned via transport 160 | 5. Client receives and processes response 161 | 162 | ### 4. UI Components 163 | 164 | **Calculator Interface:** 165 | ```html 166 |
167 | 173 | 174 | 175 | 176 |
177 |
178 | ``` 179 | 180 | **Storage Interface:** 181 | ```html 182 |
183 | 184 | 185 | 186 | 187 |
188 |
189 | ``` 190 | 191 | ## Integration Points 192 | 193 | ### 1. Server-Transport Integration 194 | 195 | **Connection Setup:** 196 | ```typescript 197 | const transport = new BrowserTransport(); 198 | await transport.start(); 199 | mcpServer = createServer(); 200 | await mcpServer.connect(transport); 201 | ``` 202 | 203 | **Message Handling:** 204 | ```typescript 205 | transport.onMessage(async (message) => { 206 | // Process incoming messages 207 | const response = await processMessage(message); 208 | transport.send(response); 209 | }); 210 | ``` 211 | 212 | ### 2. Tool-Server Integration 213 | 214 | **Tool Registration:** 215 | ```typescript 216 | server.tool( 217 | "calculate", 218 | schema, 219 | async (params) => { 220 | // Tool implementation 221 | return response; 222 | } 223 | ); 224 | ``` 225 | 226 | **Tool Execution:** 227 | ```typescript 228 | const result = await toolHandler.callback({ 229 | operation: "add", 230 | a: 5, 231 | b: 3 232 | }); 233 | ``` 234 | 235 | ### 3. Resource-Server Integration 236 | 237 | **Resource Registration:** 238 | ```typescript 239 | server.resource( 240 | "storage", 241 | "storage://{key}", 242 | async (uri, params) => { 243 | // Resource implementation 244 | return response; 245 | } 246 | ); 247 | ``` 248 | 249 | **Resource Access:** 250 | ```typescript 251 | const uri = new URL(`storage://${key}`); 252 | const result = await resourceHandler.readCallback(uri, { key }); 253 | ``` 254 | 255 | ## Development Considerations 256 | 257 | ### 1. Type Safety 258 | 259 | - Use TypeScript for type checking 260 | - Define interfaces for messages 261 | - Validate schemas using Zod 262 | - Handle type conversions 263 | 264 | ### 2. Error Handling 265 | 266 | - Graceful degradation 267 | - User-friendly error messages 268 | - Console logging for debugging 269 | - Error recovery strategies 270 | 271 | ### 3. Performance 272 | 273 | - Minimize message size 274 | - Batch operations when possible 275 | - Efficient state management 276 | - Resource cleanup 277 | 278 | ### 4. Security 279 | 280 | - Input validation 281 | - Sanitization 282 | - Scope limitation 283 | - Error message safety 284 | 285 | ## Testing Considerations 286 | 287 | ### 1. Unit Tests 288 | 289 | - Tool functionality 290 | - Resource access 291 | - Schema validation 292 | - Error handling 293 | 294 | ### 2. Integration Tests 295 | 296 | - Server-transport communication 297 | - Tool-resource interaction 298 | - UI-server integration 299 | - Error propagation 300 | 301 | ### 3. End-to-End Tests 302 | 303 | - Complete workflows 304 | - Edge cases 305 | - Error scenarios 306 | - Performance metrics 307 | 308 | ## Future Enhancements 309 | 310 | ### 1. Component Improvements 311 | 312 | - Enhanced error handling 313 | - Better type safety 314 | - Performance optimizations 315 | - Additional tools and resources 316 | 317 | ### 2. Architecture Improvements 318 | 319 | - Modular design 320 | - Plugin system 321 | - Caching layer 322 | - State management 323 | 324 | ### 3. UI Improvements 325 | 326 | - Better error display 327 | - Loading states 328 | - Input validation 329 | - Responsive design 330 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WASM MCP Server 2 | 3 | A proof-of-concept implementation of a Model Context Protocol (MCP) server that runs in WebAssembly (WASM) within a web browser. This project demonstrates the integration of MCP tools and resources in a browser environment. 4 | 5 | ## Features 6 | 7 | ### Calculator Tool 8 | - Performs basic arithmetic operations (addition, subtraction, multiplication, division) 9 | - Input validation and error handling 10 | - Real-time calculation results 11 | 12 | ### Storage System 13 | - Key-value storage functionality 14 | - Set and retrieve values using string keys 15 | - Persistent storage within the browser session 16 | - Template-based resource handling 17 | 18 | ## Technical Implementation 19 | 20 | ### Server Components 21 | - `server.ts`: Core MCP server implementation with tool and resource definitions 22 | - `main.ts`: Client-side integration and UI interaction handling 23 | - `browser-transport.ts`: Custom transport layer for browser communication 24 | 25 | ### Architecture 26 | - Uses the Model Context Protocol SDK for server implementation 27 | - Implements a custom browser transport layer 28 | - Tools are registered with callback functions 29 | - Resources use template paths with parameter substitution 30 | 31 | ### Key Concepts 32 | 1. **Tools** 33 | - Registered using `server.tool()` 34 | - Execute via callback functions 35 | - Schema validation using Zod 36 | 37 | 2. **Resources** 38 | - Template-based paths (e.g., `storage://{key}`) 39 | - Accessed via `readCallback` 40 | - Parameterized resource handling 41 | 42 | ## Usage 43 | 44 | ### Calculator 45 | 1. Select an operation (add, subtract, multiply, divide) 46 | 2. Enter two numbers 47 | 3. Click "Calculate" to see the result 48 | 4. Error handling for invalid inputs and division by zero 49 | 50 | ### Storage 51 | 1. Enter a key and value in the respective fields 52 | 2. Click "Set Storage" to store the value 53 | 3. Enter a key and click "Get Storage" to retrieve a value 54 | 4. Feedback provided for successful operations and errors 55 | 56 | ## Dependencies 57 | - @modelcontextprotocol/sdk 58 | - Zod (for schema validation) 59 | - TypeScript 60 | - Vite (for development and building) 61 | 62 | ## Project Structure 63 | ``` 64 | mcp-wasm-poc/ 65 | ├── src/ 66 | │ └── web/ 67 | │ ├── server.ts # MCP server implementation 68 | │ ├── main.ts # Client-side logic 69 | │ └── browser-transport.ts # Browser transport layer 70 | ├── index.html # Web interface 71 | └── package.json # Project dependencies 72 | ``` 73 | 74 | ## Error Handling 75 | - Server initialization errors 76 | - Tool execution errors 77 | - Resource access errors 78 | - Input validation 79 | - Transport layer errors 80 | 81 | ## Future Enhancements 82 | - Additional calculator operations 83 | - Persistent storage across sessions 84 | - Enhanced UI/UX 85 | - Additional MCP tools and resources 86 | - WASM optimization 87 | 88 | ## Development 89 | This is a proof-of-concept implementation demonstrating the feasibility of running an MCP server in a web browser using WebAssembly. The implementation focuses on demonstrating core MCP concepts while maintaining simplicity and clarity. 90 | -------------------------------------------------------------------------------- /SEQUENCE_DIAGRAMS.md: -------------------------------------------------------------------------------- 1 | # WASM MCP Server Sequence Diagrams 2 | 3 | This document illustrates the message flows between different components of the WASM MCP Server using sequence diagrams. 4 | 5 | ## Server Initialization Flow 6 | 7 | ```mermaid 8 | sequenceDiagram 9 | participant Browser 10 | participant Main 11 | participant Transport 12 | participant MCPServer 13 | 14 | Browser->>Main: Page Load 15 | activate Main 16 | Main->>Transport: new BrowserTransport() 17 | activate Transport 18 | Main->>Transport: start() 19 | Transport-->>Main: Transport Ready 20 | 21 | Main->>MCPServer: createServer() 22 | activate MCPServer 23 | MCPServer->>MCPServer: Register Calculator Tool 24 | MCPServer->>MCPServer: Register Storage Resource 25 | MCPServer->>MCPServer: Register Storage Tool 26 | MCPServer-->>Main: Server Instance 27 | 28 | Main->>MCPServer: connect(transport) 29 | MCPServer->>Transport: Establish Connection 30 | Transport-->>MCPServer: Connection Established 31 | MCPServer-->>Main: Connection Ready 32 | 33 | Main->>Browser: Enable UI Elements 34 | deactivate Main 35 | deactivate Transport 36 | deactivate MCPServer 37 | ``` 38 | 39 | ## Calculator Operation Flow 40 | 41 | ```mermaid 42 | sequenceDiagram 43 | participant UI 44 | participant Main 45 | participant MCPServer 46 | participant CalculatorTool 47 | 48 | UI->>Main: Click Calculate Button 49 | activate Main 50 | 51 | Main->>Main: Validate Server State 52 | Main->>Main: Get Input Values 53 | 54 | Main->>MCPServer: Get Tool Handler 55 | activate MCPServer 56 | MCPServer-->>Main: Calculator Tool Handler 57 | 58 | Main->>CalculatorTool: callback({operation, a, b}) 59 | activate CalculatorTool 60 | CalculatorTool->>CalculatorTool: Validate Input 61 | CalculatorTool->>CalculatorTool: Perform Calculation 62 | CalculatorTool-->>Main: Result 63 | deactivate CalculatorTool 64 | 65 | Main->>UI: Update Output Display 66 | deactivate Main 67 | deactivate MCPServer 68 | 69 | Note over UI,CalculatorTool: Error handling at each step 70 | ``` 71 | 72 | ## Storage Set Operation Flow 73 | 74 | ```mermaid 75 | sequenceDiagram 76 | participant UI 77 | participant Main 78 | participant MCPServer 79 | participant StorageTool 80 | participant StorageMap 81 | 82 | UI->>Main: Click Set Storage Button 83 | activate Main 84 | 85 | Main->>Main: Validate Server State 86 | Main->>Main: Get Key/Value 87 | 88 | Main->>MCPServer: Get Tool Handler 89 | activate MCPServer 90 | MCPServer-->>Main: Storage Tool Handler 91 | 92 | Main->>StorageTool: callback({key, value}) 93 | activate StorageTool 94 | StorageTool->>StorageTool: Validate Input 95 | StorageTool->>StorageMap: set(key, value) 96 | StorageMap-->>StorageTool: Success 97 | StorageTool-->>Main: Confirmation 98 | deactivate StorageTool 99 | 100 | Main->>UI: Update Output Display 101 | deactivate Main 102 | deactivate MCPServer 103 | ``` 104 | 105 | ## Storage Get Operation Flow 106 | 107 | ```mermaid 108 | sequenceDiagram 109 | participant UI 110 | participant Main 111 | participant MCPServer 112 | participant StorageResource 113 | participant StorageMap 114 | 115 | UI->>Main: Click Get Storage Button 116 | activate Main 117 | 118 | Main->>Main: Validate Server State 119 | Main->>Main: Get Key 120 | 121 | Main->>MCPServer: Get Resource Handler 122 | activate MCPServer 123 | MCPServer-->>Main: Storage Resource Handler 124 | 125 | Main->>Main: Create URI 126 | Main->>StorageResource: readCallback(uri, {key}) 127 | activate StorageResource 128 | StorageResource->>StorageResource: Validate Key 129 | StorageResource->>StorageMap: get(key) 130 | StorageMap-->>StorageResource: Value 131 | StorageResource-->>Main: Result 132 | deactivate StorageResource 133 | 134 | Main->>UI: Update Output Display 135 | deactivate Main 136 | deactivate MCPServer 137 | ``` 138 | 139 | ## Error Handling Flow 140 | 141 | ```mermaid 142 | sequenceDiagram 143 | participant UI 144 | participant Main 145 | participant MCPServer 146 | participant Tool 147 | 148 | UI->>Main: Action Request 149 | activate Main 150 | 151 | Main->>Main: Check Server State 152 | alt Server Not Initialized 153 | Main->>UI: Show Error: "Server not initialized" 154 | else Server Ready 155 | Main->>MCPServer: Get Handler 156 | alt Handler Not Found 157 | Main->>UI: Show Error: "Handler not found" 158 | else Handler Found 159 | Main->>Tool: Execute Operation 160 | alt Operation Success 161 | Tool-->>Main: Result 162 | Main->>UI: Show Result 163 | else Operation Error 164 | Tool-->>Main: Error 165 | Main->>UI: Show Error Message 166 | end 167 | end 168 | end 169 | deactivate Main 170 | ``` 171 | 172 | ## Transport Message Flow 173 | 174 | ```mermaid 175 | sequenceDiagram 176 | participant Client 177 | participant Transport 178 | participant WASM 179 | participant Server 180 | 181 | Client->>Transport: Send Request 182 | activate Transport 183 | 184 | Transport->>Transport: Serialize Message 185 | Transport->>WASM: Post Message 186 | activate WASM 187 | 188 | WASM->>Server: Process Request 189 | activate Server 190 | Server-->>WASM: Response 191 | deactivate Server 192 | 193 | WASM-->>Transport: Post Response 194 | deactivate WASM 195 | 196 | Transport->>Transport: Deserialize Message 197 | Transport-->>Client: Return Result 198 | deactivate Transport 199 | ``` 200 | 201 | ## Notes on the Diagrams 202 | 203 | ### Component Roles 204 | - **Browser/UI**: Handles user interactions and display 205 | - **Main**: Coordinates between UI and server components 206 | - **Transport**: Manages message passing 207 | - **MCPServer**: Core server functionality 208 | - **Tools/Resources**: Specific implementations 209 | 210 | ### Key Interactions 211 | 1. **Initialization**: One-time setup of server and transport 212 | 2. **Tool Operations**: Synchronous request-response 213 | 3. **Resource Access**: Template-based with parameters 214 | 4. **Error Handling**: At multiple levels 215 | 216 | ### Important Considerations 217 | - All operations are asynchronous 218 | - Error handling at each step 219 | - State validation before operations 220 | - Clear message flow paths 221 | - Resource cleanup 222 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # WASM MCP Server Testing Strategy 2 | 3 | This document outlines the testing approach for the WASM MCP Server implementation, covering unit tests, integration tests, and end-to-end testing. 4 | 5 | ## 1. Unit Testing 6 | 7 | ### Server Component (`server.ts`) 8 | 9 | #### Tool Registration Tests 10 | ```typescript 11 | describe('MCP Server Tool Registration', () => { 12 | let server: McpServer; 13 | 14 | beforeEach(() => { 15 | server = createServer(); 16 | }); 17 | 18 | test('should register calculator tool', () => { 19 | const tools = server._registeredTools; 20 | expect(tools).toHaveProperty('calculate'); 21 | expect(tools.calculate).toHaveProperty('callback'); 22 | }); 23 | 24 | test('should register storage tool', () => { 25 | const tools = server._registeredTools; 26 | expect(tools).toHaveProperty('set-storage'); 27 | expect(tools['set-storage']).toHaveProperty('callback'); 28 | }); 29 | }); 30 | ``` 31 | 32 | #### Calculator Tool Tests 33 | ```typescript 34 | describe('Calculator Tool', () => { 35 | let server: McpServer; 36 | let calculatorTool: any; 37 | 38 | beforeEach(() => { 39 | server = createServer(); 40 | calculatorTool = server._registeredTools.calculate; 41 | }); 42 | 43 | test('should add numbers correctly', async () => { 44 | const result = await calculatorTool.callback({ 45 | operation: 'add', 46 | a: 5, 47 | b: 3 48 | }); 49 | expect(result.content[0].text).toBe('8'); 50 | }); 51 | 52 | test('should handle division by zero', async () => { 53 | await expect(calculatorTool.callback({ 54 | operation: 'divide', 55 | a: 10, 56 | b: 0 57 | })).rejects.toThrow('Division by zero'); 58 | }); 59 | 60 | test('should validate input types', async () => { 61 | await expect(calculatorTool.callback({ 62 | operation: 'add', 63 | a: 'not a number', 64 | b: 3 65 | })).rejects.toThrow(); 66 | }); 67 | }); 68 | ``` 69 | 70 | #### Storage Tests 71 | ```typescript 72 | describe('Storage Operations', () => { 73 | let server: McpServer; 74 | let storageTool: any; 75 | let storageResource: any; 76 | 77 | beforeEach(() => { 78 | server = createServer(); 79 | storageTool = server._registeredTools['set-storage']; 80 | storageResource = server._registeredResources['storage://{key}']; 81 | }); 82 | 83 | test('should store and retrieve values', async () => { 84 | // Store value 85 | await storageTool.callback({ 86 | key: 'test-key', 87 | value: 'test-value' 88 | }); 89 | 90 | // Retrieve value 91 | const uri = new URL('storage://test-key'); 92 | const result = await storageResource.readCallback(uri, { key: 'test-key' }); 93 | expect(result.contents[0].text).toBe('test-value'); 94 | }); 95 | 96 | test('should handle missing keys', async () => { 97 | const uri = new URL('storage://nonexistent'); 98 | const result = await storageResource.readCallback(uri, { key: 'nonexistent' }); 99 | expect(result.contents[0].text).toBe('Key not found'); 100 | }); 101 | }); 102 | ``` 103 | 104 | ### Transport Component (`browser-transport.ts`) 105 | 106 | ```typescript 107 | describe('Browser Transport', () => { 108 | let transport: BrowserTransport; 109 | 110 | beforeEach(() => { 111 | transport = new BrowserTransport(); 112 | }); 113 | 114 | test('should initialize correctly', async () => { 115 | await transport.start(); 116 | expect(transport.isConnected()).toBe(true); 117 | }); 118 | 119 | test('should handle message sending', async () => { 120 | const message = { type: 'test', data: 'value' }; 121 | const response = await transport.send(message); 122 | expect(response).toBeDefined(); 123 | }); 124 | 125 | test('should handle connection errors', async () => { 126 | // Simulate connection failure 127 | jest.spyOn(transport, 'start').mockRejectedValue(new Error('Connection failed')); 128 | await expect(transport.start()).rejects.toThrow('Connection failed'); 129 | }); 130 | }); 131 | ``` 132 | 133 | ## 2. Integration Testing 134 | 135 | ### Server-Transport Integration 136 | 137 | ```typescript 138 | describe('Server-Transport Integration', () => { 139 | let server: McpServer; 140 | let transport: BrowserTransport; 141 | 142 | beforeEach(async () => { 143 | transport = new BrowserTransport(); 144 | await transport.start(); 145 | server = createServer(); 146 | await server.connect(transport); 147 | }); 148 | 149 | test('should handle tool execution through transport', async () => { 150 | const message = { 151 | type: 'tool', 152 | name: 'calculate', 153 | params: { 154 | operation: 'add', 155 | a: 5, 156 | b: 3 157 | } 158 | }; 159 | 160 | const response = await transport.send(message); 161 | expect(response.content[0].text).toBe('8'); 162 | }); 163 | 164 | test('should handle resource access through transport', async () => { 165 | // First store a value 166 | await transport.send({ 167 | type: 'tool', 168 | name: 'set-storage', 169 | params: { 170 | key: 'test-key', 171 | value: 'test-value' 172 | } 173 | }); 174 | 175 | // Then retrieve it 176 | const response = await transport.send({ 177 | type: 'resource', 178 | uri: 'storage://test-key' 179 | }); 180 | 181 | expect(response.contents[0].text).toBe('test-value'); 182 | }); 183 | }); 184 | ``` 185 | 186 | ### UI Integration Tests 187 | 188 | ```typescript 189 | describe('UI Integration', () => { 190 | beforeEach(async () => { 191 | // First set up test environment 192 | await setupTestEnvironment(); 193 | 194 | // Then initialize UI 195 | setupCalculatorUI(); 196 | }); 197 | 198 | test('should handle calculator UI interaction', async () => { 199 | const num1Input = document.getElementById('num1') as HTMLInputElement; 200 | const num2Input = document.getElementById('num2') as HTMLInputElement; 201 | const operationSelect = document.getElementById('operation') as HTMLSelectElement; 202 | const calcButton = document.getElementById('calcButton') as HTMLButtonElement; 203 | const output = document.getElementById('calcOutput'); 204 | 205 | // Test each operation 206 | const operations = [ 207 | { op: 'add', a: 5, b: 3, expected: '8' }, 208 | { op: 'subtract', a: 10, b: 4, expected: '6' }, 209 | { op: 'multiply', a: 6, b: 7, expected: '42' }, 210 | { op: 'divide', a: 15, b: 3, expected: '5' } 211 | ]; 212 | 213 | for (const { op, a, b, expected } of operations) { 214 | operationSelect.value = op; 215 | num1Input.value = a.toString(); 216 | num2Input.value = b.toString(); 217 | 218 | fireEvent.click(calcButton); 219 | 220 | // Wait for the calculation to complete 221 | await new Promise(resolve => setTimeout(resolve, 100)); 222 | 223 | expect(output?.textContent).toBe(`Result: ${expected}`); 224 | } 225 | }); 226 | 227 | test('should handle calculator error cases', async () => { 228 | const num1Input = document.getElementById('num1') as HTMLInputElement; 229 | const num2Input = document.getElementById('num2') as HTMLInputElement; 230 | const operationSelect = document.getElementById('operation') as HTMLSelectElement; 231 | const calcButton = document.getElementById('calcButton') as HTMLButtonElement; 232 | const output = document.getElementById('calcOutput'); 233 | 234 | // Test division by zero 235 | operationSelect.value = 'divide'; 236 | num1Input.value = '10'; 237 | num2Input.value = '0'; 238 | 239 | fireEvent.click(calcButton); 240 | 241 | // Wait for the error to be displayed 242 | await new Promise(resolve => setTimeout(resolve, 100)); 243 | 244 | expect(output?.textContent).toContain('Error'); 245 | expect(output?.textContent).toContain('Division by zero'); 246 | }); 247 | }); 248 | 249 | describe('Storage UI Integration', () => { 250 | beforeEach(async () => { 251 | // First set up test environment 252 | await setupTestEnvironment(); 253 | 254 | // Then initialize UI 255 | setupStorageUI(); 256 | }); 257 | 258 | test('should handle storage UI interaction', async () => { 259 | const keyInput = document.getElementById('storageKey') as HTMLInputElement; 260 | const valueInput = document.getElementById('storageValue') as HTMLInputElement; 261 | const setButton = document.getElementById('setStorageButton') as HTMLButtonElement; 262 | const getButton = document.getElementById('getStorageButton') as HTMLButtonElement; 263 | const output = document.getElementById('storageOutput'); 264 | 265 | // Set value 266 | keyInput.value = 'test-key'; 267 | valueInput.value = 'test-value'; 268 | fireEvent.click(setButton); 269 | 270 | // Wait for the storage operation to complete 271 | await new Promise(resolve => setTimeout(resolve, 100)); 272 | 273 | expect(output?.textContent).toContain('Value stored successfully'); 274 | 275 | // Get value 276 | valueInput.value = ''; 277 | fireEvent.click(getButton); 278 | 279 | // Wait for the retrieval to complete 280 | await new Promise(resolve => setTimeout(resolve, 100)); 281 | 282 | expect(output?.textContent).toContain('test-value'); 283 | }); 284 | 285 | test('should handle missing storage keys', async () => { 286 | const keyInput = document.getElementById('storageKey') as HTMLInputElement; 287 | const getButton = document.getElementById('getStorageButton') as HTMLButtonElement; 288 | const output = document.getElementById('storageOutput'); 289 | 290 | keyInput.value = 'nonexistent-key'; 291 | fireEvent.click(getButton); 292 | 293 | // Wait for the retrieval to complete 294 | await new Promise(resolve => setTimeout(resolve, 100)); 295 | 296 | expect(output?.textContent).toContain('Key not found'); 297 | }); 298 | }); 299 | ``` 300 | 301 | ## 3. End-to-End Testing 302 | 303 | ### Test Scenarios 304 | 305 | ```typescript 306 | describe('End-to-End Workflows', () => { 307 | beforeEach(async () => { 308 | // Set up complete environment 309 | await setupTestEnvironment(); 310 | }); 311 | 312 | test('complete calculator workflow', async () => { 313 | // Test all operations 314 | const operations = ['add', 'subtract', 'multiply', 'divide']; 315 | const testCases = [ 316 | { a: 5, b: 3, expected: ['8', '2', '15', '1.6666666666666667'] } 317 | ]; 318 | 319 | for (const { a, b, expected } of testCases) { 320 | for (let i = 0; i < operations.length; i++) { 321 | const operation = operations[i]; 322 | const result = await executeCalculation(operation, a, b); 323 | expect(result).toBe(expected[i]); 324 | } 325 | } 326 | }); 327 | 328 | test('complete storage workflow', async () => { 329 | // Store multiple values 330 | const testData = [ 331 | { key: 'key1', value: 'value1' }, 332 | { key: 'key2', value: 'value2' } 333 | ]; 334 | 335 | for (const { key, value } of testData) { 336 | await setStorageValue(key, value); 337 | const retrieved = await getStorageValue(key); 338 | expect(retrieved).toBe(value); 339 | } 340 | }); 341 | }); 342 | ``` 343 | 344 | ## 4. Performance Testing 345 | 346 | ```typescript 347 | describe('Performance Tests', () => { 348 | test('calculator performance', async () => { 349 | const startTime = performance.now(); 350 | 351 | // Perform 1000 calculations 352 | for (let i = 0; i < 1000; i++) { 353 | await executeCalculation('add', i, i + 1); 354 | } 355 | 356 | const endTime = performance.now(); 357 | const duration = endTime - startTime; 358 | 359 | expect(duration).toBeLessThan(1000); // Should complete in less than 1 second 360 | }); 361 | 362 | test('storage performance', async () => { 363 | const startTime = performance.now(); 364 | 365 | // Perform 1000 storage operations 366 | for (let i = 0; i < 1000; i++) { 367 | await setStorageValue(`key${i}`, `value${i}`); 368 | await getStorageValue(`key${i}`); 369 | } 370 | 371 | const endTime = performance.now(); 372 | const duration = endTime - startTime; 373 | 374 | expect(duration).toBeLessThan(2000); // Should complete in less than 2 seconds 375 | }); 376 | }); 377 | ``` 378 | 379 | ## 5. Error Testing 380 | 381 | ```typescript 382 | describe('Error Handling', () => { 383 | test('server initialization errors', async () => { 384 | // Test invalid server configuration 385 | expect(() => { 386 | new McpServer({ name: '', version: '' }); 387 | }).toThrow(); 388 | }); 389 | 390 | test('transport errors', async () => { 391 | // Test connection failures 392 | const transport = new BrowserTransport(); 393 | jest.spyOn(transport, 'start').mockRejectedValue(new Error('Network error')); 394 | 395 | await expect(transport.start()).rejects.toThrow('Network error'); 396 | }); 397 | 398 | test('calculator errors', async () => { 399 | // Test various error conditions 400 | const errorCases = [ 401 | { operation: 'divide', a: 1, b: 0, error: 'Division by zero' }, 402 | { operation: 'invalid', a: 1, b: 1, error: 'Invalid operation' }, 403 | { operation: 'add', a: 'invalid', b: 1, error: 'Invalid input' } 404 | ]; 405 | 406 | for (const { operation, a, b, error } of errorCases) { 407 | await expect(executeCalculation(operation, a, b)).rejects.toThrow(error); 408 | } 409 | }); 410 | }); 411 | ``` 412 | 413 | ## Test Configuration 414 | 415 | ### Jest Configuration 416 | ```javascript 417 | // jest.config.js 418 | module.exports = { 419 | preset: 'ts-jest', 420 | testEnvironment: 'jsdom', 421 | setupFilesAfterEnv: ['./jest.setup.ts'], 422 | moduleNameMapper: { 423 | '^@/(.*)$': '/src/$1' 424 | }, 425 | coverageThreshold: { 426 | global: { 427 | branches: 80, 428 | functions: 80, 429 | lines: 80, 430 | statements: 80 431 | } 432 | } 433 | }; 434 | ``` 435 | 436 | ### Test Setup 437 | ```typescript 438 | // jest.setup.ts 439 | import '@testing-library/jest-dom'; 440 | 441 | global.beforeEach(() => { 442 | // Reset DOM 443 | document.body.innerHTML = ''; 444 | 445 | // Reset storage 446 | localStorage.clear(); 447 | 448 | // Reset server state 449 | jest.resetModules(); 450 | }); 451 | ``` 452 | 453 | ## Running Tests 454 | 455 | ```bash 456 | # Run all tests 457 | npm test 458 | 459 | # Run tests with coverage 460 | npm test -- --coverage 461 | 462 | # Run specific test file 463 | npm test -- server.test.ts 464 | 465 | # Run tests in watch mode 466 | npm test -- --watch 467 | ``` 468 | 469 | ## Continuous Integration 470 | 471 | ```yaml 472 | # .github/workflows/test.yml 473 | name: Tests 474 | 475 | on: [push, pull_request] 476 | 477 | jobs: 478 | test: 479 | runs-on: ubuntu-latest 480 | 481 | steps: 482 | - uses: actions/checkout@v2 483 | - uses: actions/setup-node@v2 484 | with: 485 | node-version: '16' 486 | 487 | - name: Install dependencies 488 | run: npm ci 489 | 490 | - name: Run tests 491 | run: npm test -- --coverage 492 | 493 | - name: Upload coverage 494 | uses: codecov/codecov-action@v2 495 | ``` 496 | 497 | ## Test Coverage Requirements 498 | 499 | - Minimum 80% line coverage 500 | - Minimum 80% branch coverage 501 | - Minimum 80% function coverage 502 | - Critical paths must have 100% coverage: 503 | - Server initialization 504 | - Tool registration 505 | - Resource handling 506 | - Error handling 507 | 508 | ## Manual Testing Checklist 509 | 510 | 1. Server Initialization 511 | - [ ] Server starts successfully 512 | - [ ] Tools are registered 513 | - [ ] Resources are available 514 | - [ ] UI is enabled 515 | 516 | 2. Calculator Operations 517 | - [ ] All operations work correctly 518 | - [ ] Error handling works 519 | - [ ] UI updates properly 520 | - [ ] Performance is acceptable 521 | 522 | 3. Storage Operations 523 | - [ ] Can store values 524 | - [ ] Can retrieve values 525 | - [ ] Handles missing keys 526 | - [ ] Updates UI correctly 527 | 528 | 4. Error Scenarios 529 | - [ ] Server initialization failures 530 | - [ ] Network errors 531 | - [ ] Invalid inputs 532 | - [ ] Resource not found 533 | 534 | 5. Browser Compatibility 535 | - [ ] Chrome 536 | - [ ] Firefox 537 | - [ ] Safari 538 | - [ ] Edge 539 | -------------------------------------------------------------------------------- /asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "debug": { 4 | "outFile": "build/debug.wasm", 5 | "textFile": "build/debug.wat", 6 | "sourceMap": true, 7 | "debug": true 8 | }, 9 | "release": { 10 | "outFile": "build/release.wasm", 11 | "textFile": "build/release.wat", 12 | "sourceMap": true, 13 | "optimizeLevel": 3, 14 | "shrinkLevel": 0, 15 | "converge": false, 16 | "noAssert": false 17 | } 18 | }, 19 | "options": { 20 | "bindings": "esm" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | setupFilesAfterEnv: ['./jest.setup.ts'], 5 | moduleNameMapper: { 6 | '^@/(.*)$': '/src/$1' 7 | }, 8 | coverageThreshold: { 9 | global: { 10 | branches: 80, 11 | functions: 80, 12 | lines: 80, 13 | statements: 80 14 | } 15 | }, 16 | transform: { 17 | '^.+\\.tsx?$': ['ts-jest', { 18 | useESM: true 19 | }] 20 | }, 21 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 22 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 23 | }; 24 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | // Mock URL since it's not available in jsdom 4 | global.URL = class URL { 5 | href: string; 6 | constructor(url: string) { 7 | this.href = url; 8 | } 9 | } as any; 10 | 11 | // Reset environment before each test 12 | beforeEach(() => { 13 | // Reset DOM 14 | document.body.innerHTML = ''; 15 | 16 | // Reset storage 17 | localStorage.clear(); 18 | 19 | // Reset server state 20 | jest.resetModules(); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-wasm-poc", 3 | "version": "1.0.0", 4 | "description": "WASM-based Model Context Protocol Server POC", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "test:coverage": "jest --coverage", 13 | "asbuild:debug": "asc src/wasm/mcp-server.ts --target debug", 14 | "asbuild:release": "asc src/wasm/mcp-server.ts --target release", 15 | "asbuild": "npm run asbuild:debug && npm run asbuild:release" 16 | }, 17 | "keywords": [ 18 | "mcp", 19 | "wasm", 20 | "webassembly" 21 | ], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@modelcontextprotocol/sdk": "latest", 26 | "zod": "^3.22.4" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/dom": "^10.4.0", 30 | "@testing-library/jest-dom": "^6.6.3", 31 | "@types/jest": "^29.5.14", 32 | "@types/node": "latest", 33 | "assemblyscript": "latest", 34 | "jest": "^29.7.0", 35 | "jest-environment-jsdom": "^29.7.0", 36 | "ts-jest": "^29.2.5", 37 | "typescript": "latest", 38 | "vite": "latest" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/wasm/mcp-server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | 4 | // Create the MCP server instance 5 | export function createServer(): McpServer { 6 | const server = new McpServer({ 7 | name: "WASM MCP Server", 8 | version: "1.0.0" 9 | }); 10 | 11 | // Add a simple calculator tool 12 | server.tool( 13 | "calculate", 14 | { 15 | operation: z.enum(["add", "subtract", "multiply", "divide"]), 16 | a: z.number(), 17 | b: z.number() 18 | }, 19 | async ({ operation, a, b }) => { 20 | let result: number; 21 | switch (operation) { 22 | case "add": 23 | result = a + b; 24 | break; 25 | case "subtract": 26 | result = a - b; 27 | break; 28 | case "multiply": 29 | result = a * b; 30 | break; 31 | case "divide": 32 | if (b === 0) throw new Error("Division by zero"); 33 | result = a / b; 34 | break; 35 | } 36 | return { 37 | content: [{ type: "text", text: String(result) }] 38 | }; 39 | } 40 | ); 41 | 42 | // Add a simple storage resource 43 | const storage = new Map(); 44 | server.resource( 45 | "storage", 46 | "storage://{key}", 47 | async (uri, { key }) => ({ 48 | contents: [{ 49 | uri: uri.href, 50 | text: storage.get(key) || "Key not found" 51 | }] 52 | }) 53 | ); 54 | 55 | // Add a storage tool 56 | server.tool( 57 | "set-storage", 58 | { 59 | key: z.string(), 60 | value: z.string() 61 | }, 62 | async ({ key, value }) => { 63 | storage.set(key, value); 64 | return { 65 | content: [{ type: "text", text: `Stored value at key: ${key}` }] 66 | }; 67 | } 68 | ); 69 | 70 | return server; 71 | } 72 | -------------------------------------------------------------------------------- /src/web/__tests__/browser-transport.test.ts: -------------------------------------------------------------------------------- 1 | import { BrowserTransport } from '../browser-transport'; 2 | 3 | describe('BrowserTransport', () => { 4 | let transport: BrowserTransport; 5 | 6 | beforeEach(() => { 7 | transport = new BrowserTransport(true); // Enable test mode 8 | }); 9 | 10 | afterEach(async () => { 11 | await transport.stop(); 12 | }); 13 | 14 | test('should start and stop correctly', async () => { 15 | await transport.start(); 16 | expect(transport.isConnected()).toBe(true); 17 | 18 | await transport.stop(); 19 | expect(transport.isConnected()).toBe(false); 20 | }); 21 | 22 | test('should send messages when connected', async () => { 23 | await transport.start(); 24 | const message = { 25 | jsonrpc: "2.0" as const, 26 | method: "test", 27 | id: 1, 28 | params: { 29 | data: "test data" 30 | } 31 | }; 32 | 33 | const mockCallback = jest.fn(); 34 | transport.onMessage(mockCallback); 35 | await transport.send(message); 36 | 37 | expect(mockCallback).toHaveBeenCalledWith(message); 38 | }); 39 | 40 | test('should handle multiple message callbacks', async () => { 41 | await transport.start(); 42 | const message = { 43 | jsonrpc: "2.0" as const, 44 | method: "test", 45 | id: 1, 46 | params: { 47 | data: "test data" 48 | } 49 | }; 50 | 51 | const mockCallback1 = jest.fn(); 52 | const mockCallback2 = jest.fn(); 53 | 54 | transport.onMessage(mockCallback1); 55 | transport.onMessage(mockCallback2); 56 | 57 | await transport.send(message); 58 | 59 | expect(mockCallback1).toHaveBeenCalledWith(message); 60 | expect(mockCallback2).toHaveBeenCalledWith(message); 61 | }); 62 | 63 | test('should throw error when sending message while not connected', async () => { 64 | const message = { 65 | jsonrpc: "2.0" as const, 66 | method: "test", 67 | id: 1, 68 | params: { 69 | data: "test data" 70 | } 71 | }; 72 | 73 | await expect(transport.send(message)).rejects.toThrow('Transport not connected'); 74 | }); 75 | 76 | test('should remove message callback when cleanup function is called', async () => { 77 | await transport.start(); 78 | const message = { 79 | jsonrpc: "2.0" as const, 80 | method: "test", 81 | id: 1, 82 | params: { 83 | data: "test data" 84 | } 85 | }; 86 | 87 | const mockCallback = jest.fn(); 88 | const cleanup = transport.onMessage(mockCallback); 89 | await transport.send(message); 90 | expect(mockCallback).toHaveBeenCalledWith(message); 91 | 92 | mockCallback.mockClear(); 93 | cleanup(); 94 | await transport.send(message); 95 | expect(mockCallback).not.toHaveBeenCalled(); 96 | }); 97 | 98 | test('should handle multiple messages in sequence', async () => { 99 | await transport.start(); 100 | const messages = [ 101 | { 102 | jsonrpc: "2.0" as const, 103 | method: "test1", 104 | id: 1, 105 | params: { 106 | data: "test data 1" 107 | } 108 | }, 109 | { 110 | jsonrpc: "2.0" as const, 111 | method: "test2", 112 | id: 2, 113 | params: { 114 | data: "test data 2" 115 | } 116 | } 117 | ]; 118 | 119 | const mockCallback = jest.fn(); 120 | transport.onMessage(mockCallback); 121 | 122 | for (const message of messages) { 123 | await transport.send(message); 124 | } 125 | 126 | expect(mockCallback).toHaveBeenCalledTimes(2); 127 | expect(mockCallback).toHaveBeenNthCalledWith(1, messages[0]); 128 | expect(mockCallback).toHaveBeenNthCalledWith(2, messages[1]); 129 | }); 130 | 131 | test('should handle window message events', async () => { 132 | await transport.start(); 133 | const message = { 134 | jsonrpc: "2.0" as const, 135 | method: "test", 136 | id: 1, 137 | params: { 138 | data: "test data" 139 | } 140 | }; 141 | 142 | const mockCallback = jest.fn(); 143 | transport.onMessage(mockCallback); 144 | 145 | window.postMessage({ 146 | type: "mcp-message", 147 | message 148 | }, "*"); 149 | 150 | await new Promise(resolve => setTimeout(resolve, 0)); 151 | 152 | expect(mockCallback).toHaveBeenCalledWith(message); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/web/__tests__/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { setupTestEnvironment, setupCalculatorUI, setupStorageUI } from './test-utils'; 2 | import { fireEvent } from '@testing-library/dom'; 3 | 4 | describe('Integration Tests', () => { 5 | describe('Server-Transport Integration', () => { 6 | test('should handle tool execution through transport', async () => { 7 | const { transport, server } = await setupTestEnvironment(); 8 | 9 | const message = { 10 | jsonrpc: "2.0" as const, 11 | method: "tool", 12 | id: 1, 13 | params: { 14 | name: "calculate", 15 | params: { 16 | operation: "add", 17 | a: 5, 18 | b: 3 19 | } 20 | } 21 | }; 22 | 23 | const mockCallback = jest.fn(); 24 | transport.onMessage(mockCallback); 25 | await transport.send(message); 26 | 27 | // Wait for the response 28 | await new Promise(resolve => setTimeout(resolve, 100)); 29 | 30 | expect(mockCallback).toHaveBeenCalled(); 31 | const response = mockCallback.mock.calls[0][0]; 32 | expect(response.result.content[0].text).toBe('8'); 33 | }); 34 | 35 | test('should handle resource access through transport', async () => { 36 | const { transport, server } = await setupTestEnvironment(); 37 | 38 | // First store a value 39 | await transport.send({ 40 | jsonrpc: "2.0" as const, 41 | method: "tool", 42 | id: 1, 43 | params: { 44 | name: "set-storage", 45 | params: { 46 | key: "test-key", 47 | value: "test-value" 48 | } 49 | } 50 | }); 51 | 52 | // Wait for the storage operation to complete 53 | await new Promise(resolve => setTimeout(resolve, 100)); 54 | 55 | // Then retrieve it 56 | const mockCallback = jest.fn(); 57 | transport.onMessage(mockCallback); 58 | 59 | await transport.send({ 60 | jsonrpc: "2.0" as const, 61 | method: "resource", 62 | id: 2, 63 | params: { 64 | uri: "storage://test-key", 65 | key: "test-key" 66 | } 67 | }); 68 | 69 | // Wait for the response 70 | await new Promise(resolve => setTimeout(resolve, 100)); 71 | 72 | expect(mockCallback).toHaveBeenCalled(); 73 | const response = mockCallback.mock.calls[0][0]; 74 | expect(response.result.contents[0].text).toBe('test-value'); 75 | }); 76 | }); 77 | 78 | describe('UI Integration', () => { 79 | beforeEach(async () => { 80 | await setupTestEnvironment(); 81 | setupCalculatorUI(); 82 | }); 83 | 84 | test('should handle calculator UI interaction', async () => { 85 | // Set up inputs 86 | const num1Input = document.getElementById('num1') as HTMLInputElement; 87 | const num2Input = document.getElementById('num2') as HTMLInputElement; 88 | const operationSelect = document.getElementById('operation') as HTMLSelectElement; 89 | const calcButton = document.getElementById('calcButton') as HTMLButtonElement; 90 | const output = document.getElementById('calcOutput'); 91 | 92 | // Test each operation 93 | const operations = [ 94 | { op: 'add', a: 5, b: 3, expected: '8' }, 95 | { op: 'subtract', a: 10, b: 4, expected: '6' }, 96 | { op: 'multiply', a: 6, b: 7, expected: '42' }, 97 | { op: 'divide', a: 15, b: 3, expected: '5' } 98 | ]; 99 | 100 | for (const { op, a, b, expected } of operations) { 101 | operationSelect.value = op; 102 | num1Input.value = a.toString(); 103 | num2Input.value = b.toString(); 104 | 105 | fireEvent.click(calcButton); 106 | 107 | // Wait for the calculation to complete 108 | await new Promise(resolve => setTimeout(resolve, 100)); 109 | 110 | expect(output?.textContent).toBe(`Result: ${expected}`); 111 | } 112 | }); 113 | 114 | test('should handle calculator error cases', async () => { 115 | const num1Input = document.getElementById('num1') as HTMLInputElement; 116 | const num2Input = document.getElementById('num2') as HTMLInputElement; 117 | const operationSelect = document.getElementById('operation') as HTMLSelectElement; 118 | const calcButton = document.getElementById('calcButton') as HTMLButtonElement; 119 | const output = document.getElementById('calcOutput'); 120 | 121 | // Test division by zero 122 | operationSelect.value = 'divide'; 123 | num1Input.value = '10'; 124 | num2Input.value = '0'; 125 | 126 | fireEvent.click(calcButton); 127 | 128 | // Wait for the error to be displayed 129 | await new Promise(resolve => setTimeout(resolve, 100)); 130 | 131 | expect(output?.textContent).toContain('Error'); 132 | expect(output?.textContent).toContain('Division by zero'); 133 | }); 134 | }); 135 | 136 | describe('Storage UI Integration', () => { 137 | beforeEach(async () => { 138 | await setupTestEnvironment(); 139 | setupStorageUI(); 140 | }); 141 | 142 | test('should handle storage UI interaction', async () => { 143 | const keyInput = document.getElementById('storageKey') as HTMLInputElement; 144 | const valueInput = document.getElementById('storageValue') as HTMLInputElement; 145 | const setButton = document.getElementById('setStorageButton') as HTMLButtonElement; 146 | const getButton = document.getElementById('getStorageButton') as HTMLButtonElement; 147 | const output = document.getElementById('storageOutput'); 148 | 149 | // Set value 150 | keyInput.value = 'test-key'; 151 | valueInput.value = 'test-value'; 152 | fireEvent.click(setButton); 153 | 154 | // Wait for the storage operation to complete 155 | await new Promise(resolve => setTimeout(resolve, 100)); 156 | 157 | expect(output?.textContent).toContain('Value stored successfully'); 158 | 159 | // Get value 160 | valueInput.value = ''; 161 | fireEvent.click(getButton); 162 | 163 | // Wait for the retrieval to complete 164 | await new Promise(resolve => setTimeout(resolve, 100)); 165 | 166 | expect(output?.textContent).toContain('test-value'); 167 | }); 168 | 169 | test('should handle missing storage keys', async () => { 170 | const keyInput = document.getElementById('storageKey') as HTMLInputElement; 171 | const getButton = document.getElementById('getStorageButton') as HTMLButtonElement; 172 | const output = document.getElementById('storageOutput'); 173 | 174 | keyInput.value = 'nonexistent-key'; 175 | fireEvent.click(getButton); 176 | 177 | // Wait for the retrieval to complete 178 | await new Promise(resolve => setTimeout(resolve, 100)); 179 | 180 | expect(output?.textContent).toContain('Key not found'); 181 | }); 182 | }); 183 | 184 | describe('End-to-End Workflows', () => { 185 | test('complete calculator workflow', async () => { 186 | const { transport } = await setupTestEnvironment(); 187 | 188 | const operations = ['add', 'subtract', 'multiply', 'divide']; 189 | const testCases = [ 190 | { a: 5, b: 3, expected: ['8', '2', '15', '1.6666666666666667'] } 191 | ]; 192 | 193 | for (const { a, b, expected } of testCases) { 194 | for (let i = 0; i < operations.length; i++) { 195 | const message = { 196 | jsonrpc: "2.0" as const, 197 | method: "tool", 198 | id: i + 1, 199 | params: { 200 | name: "calculate", 201 | params: { 202 | operation: operations[i], 203 | a, 204 | b 205 | } 206 | } 207 | }; 208 | 209 | const mockCallback = jest.fn(); 210 | transport.onMessage(mockCallback); 211 | await transport.send(message); 212 | 213 | // Wait for the calculation to complete 214 | await new Promise(resolve => setTimeout(resolve, 100)); 215 | 216 | expect(mockCallback).toHaveBeenCalled(); 217 | const response = mockCallback.mock.calls[0][0]; 218 | expect(response.result.content[0].text).toBe(expected[i]); 219 | } 220 | } 221 | }); 222 | 223 | test('complete storage workflow', async () => { 224 | const { transport } = await setupTestEnvironment(); 225 | 226 | const testData = [ 227 | { key: 'key1', value: 'value1' }, 228 | { key: 'key2', value: 'value2' } 229 | ]; 230 | 231 | for (const { key, value } of testData) { 232 | // Store value 233 | await transport.send({ 234 | jsonrpc: "2.0" as const, 235 | method: "tool", 236 | id: 1, 237 | params: { 238 | name: "set-storage", 239 | params: { key, value } 240 | } 241 | }); 242 | 243 | // Wait for the storage operation to complete 244 | await new Promise(resolve => setTimeout(resolve, 100)); 245 | 246 | // Retrieve value 247 | const mockCallback = jest.fn(); 248 | transport.onMessage(mockCallback); 249 | 250 | await transport.send({ 251 | jsonrpc: "2.0" as const, 252 | method: "resource", 253 | id: 2, 254 | params: { 255 | uri: `storage://${key}`, 256 | key 257 | } 258 | }); 259 | 260 | // Wait for the retrieval to complete 261 | await new Promise(resolve => setTimeout(resolve, 100)); 262 | 263 | expect(mockCallback).toHaveBeenCalled(); 264 | const response = mockCallback.mock.calls[0][0]; 265 | expect(response.result.contents[0].text).toBe(value); 266 | } 267 | }); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /src/web/__tests__/server.test.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { createServer } from '../server'; 3 | import { getRegisteredTools, getRegisteredResources, getToolHandler, getResourceHandler } from './test-helpers'; 4 | 5 | describe('MCP Server', () => { 6 | describe('Server Creation', () => { 7 | let server: McpServer; 8 | 9 | beforeEach(() => { 10 | server = createServer(); 11 | }); 12 | 13 | test('should create server instance', () => { 14 | expect(server).toBeInstanceOf(McpServer); 15 | }); 16 | 17 | test('should register calculator tool', () => { 18 | const tools = getRegisteredTools(server); 19 | expect(tools).toHaveProperty('calculate'); 20 | expect(tools.calculate).toHaveProperty('callback'); 21 | }); 22 | 23 | test('should register storage tool', () => { 24 | const tools = getRegisteredTools(server); 25 | expect(tools).toHaveProperty('set-storage'); 26 | expect(tools['set-storage']).toHaveProperty('callback'); 27 | }); 28 | 29 | test('should register storage resource', () => { 30 | const resources = getRegisteredResources(server); 31 | expect(resources).toHaveProperty('storage://{key}'); 32 | expect(resources['storage://{key}']).toHaveProperty('readCallback'); 33 | }); 34 | }); 35 | 36 | describe('Calculator Tool', () => { 37 | let server: McpServer; 38 | 39 | beforeEach(() => { 40 | server = createServer(); 41 | }); 42 | 43 | test('should add numbers correctly', async () => { 44 | const calculatorTool = getToolHandler(server, 'calculate'); 45 | const result = await calculatorTool.callback({ 46 | operation: 'add', 47 | a: 5, 48 | b: 3 49 | }); 50 | expect(result.content[0].text).toBe('8'); 51 | }); 52 | 53 | test('should subtract numbers correctly', async () => { 54 | const calculatorTool = getToolHandler(server, 'calculate'); 55 | const result = await calculatorTool.callback({ 56 | operation: 'subtract', 57 | a: 5, 58 | b: 3 59 | }); 60 | expect(result.content[0].text).toBe('2'); 61 | }); 62 | 63 | test('should multiply numbers correctly', async () => { 64 | const calculatorTool = getToolHandler(server, 'calculate'); 65 | const result = await calculatorTool.callback({ 66 | operation: 'multiply', 67 | a: 5, 68 | b: 3 69 | }); 70 | expect(result.content[0].text).toBe('15'); 71 | }); 72 | 73 | test('should divide numbers correctly', async () => { 74 | const calculatorTool = getToolHandler(server, 'calculate'); 75 | const result = await calculatorTool.callback({ 76 | operation: 'divide', 77 | a: 6, 78 | b: 2 79 | }); 80 | expect(result.content[0].text).toBe('3'); 81 | }); 82 | 83 | test('should handle division by zero', async () => { 84 | const calculatorTool = getToolHandler(server, 'calculate'); 85 | await expect(calculatorTool.callback({ 86 | operation: 'divide', 87 | a: 6, 88 | b: 0 89 | })).rejects.toThrow('Division by zero'); 90 | }); 91 | 92 | test('should validate operation type', async () => { 93 | const calculatorTool = getToolHandler(server, 'calculate'); 94 | await expect(calculatorTool.callback({ 95 | operation: 'invalid' as any, 96 | a: 5, 97 | b: 3 98 | })).rejects.toThrow(); 99 | }); 100 | 101 | test('should validate number inputs', async () => { 102 | const calculatorTool = getToolHandler(server, 'calculate'); 103 | await expect(calculatorTool.callback({ 104 | operation: 'add', 105 | a: 'not a number' as any, 106 | b: 3 107 | })).rejects.toThrow(); 108 | }); 109 | }); 110 | 111 | describe('Storage Operations', () => { 112 | let server: McpServer; 113 | 114 | beforeEach(() => { 115 | server = createServer(); 116 | }); 117 | 118 | test('should store and retrieve values', async () => { 119 | const storageTool = getToolHandler(server, 'set-storage'); 120 | const storageResource = getResourceHandler(server, 'storage://{key}'); 121 | 122 | // Store value 123 | await storageTool.callback({ 124 | key: 'test-key', 125 | value: 'test-value' 126 | }); 127 | 128 | // Retrieve value 129 | const uri = new URL('storage://test-key'); 130 | const result = await storageResource.readCallback(uri, { key: 'test-key' }); 131 | expect(result.contents[0].text).toBe('test-value'); 132 | }); 133 | 134 | test('should handle missing keys', async () => { 135 | const storageResource = getResourceHandler(server, 'storage://{key}'); 136 | 137 | const uri = new URL('storage://nonexistent'); 138 | const result = await storageResource.readCallback(uri, { key: 'nonexistent' }); 139 | expect(result.contents[0].text).toBe('Key not found'); 140 | }); 141 | 142 | test('should update existing values', async () => { 143 | const storageTool = getToolHandler(server, 'set-storage'); 144 | const storageResource = getResourceHandler(server, 'storage://{key}'); 145 | 146 | // Store initial value 147 | await storageTool.callback({ 148 | key: 'test-key', 149 | value: 'initial-value' 150 | }); 151 | 152 | // Update value 153 | await storageTool.callback({ 154 | key: 'test-key', 155 | value: 'updated-value' 156 | }); 157 | 158 | // Retrieve updated value 159 | const uri = new URL('storage://test-key'); 160 | const result = await storageResource.readCallback(uri, { key: 'test-key' }); 161 | expect(result.contents[0].text).toBe('updated-value'); 162 | }); 163 | 164 | test('should validate storage inputs', async () => { 165 | const storageTool = getToolHandler(server, 'set-storage'); 166 | 167 | await expect(storageTool.callback({ 168 | key: 123 as any, 169 | value: 'test-value' 170 | })).rejects.toThrow(); 171 | 172 | await expect(storageTool.callback({ 173 | key: 'test-key', 174 | value: null as any 175 | })).rejects.toThrow(); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /src/web/__tests__/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | 3 | // Helper to access private properties for testing 4 | export function getPrivateProperty(obj: any, prop: string) { 5 | return obj[prop]; 6 | } 7 | 8 | // Helper to get registered tools 9 | export function getRegisteredTools(server: McpServer) { 10 | return getPrivateProperty(server, '_registeredTools'); 11 | } 12 | 13 | // Helper to get registered resources 14 | export function getRegisteredResources(server: McpServer) { 15 | return getPrivateProperty(server, '_registeredResources'); 16 | } 17 | 18 | // Helper to get tool handler 19 | export function getToolHandler(server: McpServer, toolName: string) { 20 | const tools = getRegisteredTools(server); 21 | return tools[toolName]; 22 | } 23 | 24 | // Helper to get resource handler 25 | export function getResourceHandler(server: McpServer, resourcePath: string) { 26 | const resources = getRegisteredResources(server); 27 | return resources[resourcePath]; 28 | } 29 | 30 | // Tests for test helpers 31 | describe('Test Helpers', () => { 32 | const mockServer = { 33 | _registeredTools: { 34 | test: { callback: () => {} } 35 | }, 36 | _registeredResources: { 37 | 'test://{param}': { readCallback: () => {} } 38 | } 39 | }; 40 | 41 | test('getPrivateProperty should access private properties', () => { 42 | const tools = getPrivateProperty(mockServer, '_registeredTools'); 43 | expect(tools).toBeDefined(); 44 | expect(tools.test).toBeDefined(); 45 | }); 46 | 47 | test('getRegisteredTools should return tools', () => { 48 | const tools = getRegisteredTools(mockServer as any); 49 | expect(tools).toBeDefined(); 50 | expect(tools.test).toBeDefined(); 51 | }); 52 | 53 | test('getRegisteredResources should return resources', () => { 54 | const resources = getRegisteredResources(mockServer as any); 55 | expect(resources).toBeDefined(); 56 | expect(resources['test://{param}']).toBeDefined(); 57 | }); 58 | 59 | test('getToolHandler should return specific tool', () => { 60 | const tool = getToolHandler(mockServer as any, 'test'); 61 | expect(tool).toBeDefined(); 62 | expect(tool.callback).toBeDefined(); 63 | }); 64 | 65 | test('getResourceHandler should return specific resource', () => { 66 | const resource = getResourceHandler(mockServer as any, 'test://{param}'); 67 | expect(resource).toBeDefined(); 68 | expect(resource.readCallback).toBeDefined(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/web/__tests__/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import { BrowserTransport } from '../browser-transport'; 4 | import { createServer } from '../server'; 5 | import { getToolHandler, getResourceHandler } from './test-helpers'; 6 | 7 | let testEnvironment: { transport: BrowserTransport; server: McpServer } | null = null; 8 | 9 | export async function setupTestEnvironment() { 10 | if (testEnvironment) { 11 | await testEnvironment.transport.stop(); 12 | } 13 | 14 | const transport = new BrowserTransport(false); // Disable test mode to use actual handlers 15 | const server = createServer(); 16 | 17 | await transport.start(); 18 | await server.connect(transport); 19 | 20 | testEnvironment = { transport, server }; 21 | return testEnvironment; 22 | } 23 | 24 | export function setupCalculatorUI() { 25 | document.body.innerHTML = ` 26 |
27 | 28 | 29 | 35 | 36 |
37 |
38 | `; 39 | 40 | const calcButton = document.getElementById('calcButton'); 41 | const output = document.getElementById('calcOutput'); 42 | 43 | if (!calcButton || !output) { 44 | throw new Error('Calculator UI elements not found'); 45 | } 46 | 47 | calcButton.addEventListener('click', async () => { 48 | const operation = (document.getElementById('operation') as HTMLSelectElement).value; 49 | const a = parseFloat((document.getElementById('num1') as HTMLInputElement).value); 50 | const b = parseFloat((document.getElementById('num2') as HTMLInputElement).value); 51 | 52 | try { 53 | const { transport } = testEnvironment!; 54 | const message = { 55 | jsonrpc: "2.0" as const, 56 | method: "tool", 57 | id: 1, 58 | params: { 59 | name: "calculate", 60 | params: { 61 | operation, 62 | a, 63 | b 64 | } 65 | } 66 | }; 67 | 68 | transport.onMessage((response) => { 69 | if (response.result && response.result.content) { 70 | output.textContent = `Result: ${response.result.content[0].text}`; 71 | } else if (response.error) { 72 | output.textContent = `Error: ${response.error.message}`; 73 | } 74 | }); 75 | 76 | await transport.send(message); 77 | } catch (error) { 78 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 79 | } 80 | }); 81 | } 82 | 83 | export function setupStorageUI() { 84 | document.body.innerHTML = ` 85 |
86 | 87 | 88 | 89 | 90 |
91 |
92 | `; 93 | 94 | const setButton = document.getElementById('setStorageButton'); 95 | const getButton = document.getElementById('getStorageButton'); 96 | const output = document.getElementById('storageOutput'); 97 | 98 | if (!setButton || !getButton || !output) { 99 | throw new Error('Storage UI elements not found'); 100 | } 101 | 102 | setButton.addEventListener('click', async () => { 103 | const key = (document.getElementById('storageKey') as HTMLInputElement).value; 104 | const value = (document.getElementById('storageValue') as HTMLInputElement).value; 105 | 106 | try { 107 | const { transport } = testEnvironment!; 108 | const message = { 109 | jsonrpc: "2.0" as const, 110 | method: "tool", 111 | id: 1, 112 | params: { 113 | name: "set-storage", 114 | params: { 115 | key, 116 | value 117 | } 118 | } 119 | }; 120 | 121 | transport.onMessage((response) => { 122 | if (response.result && response.result.content) { 123 | output.textContent = response.result.content[0].text; 124 | } else if (response.error) { 125 | output.textContent = `Error: ${response.error.message}`; 126 | } 127 | }); 128 | 129 | await transport.send(message); 130 | } catch (error) { 131 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 132 | } 133 | }); 134 | 135 | getButton.addEventListener('click', async () => { 136 | const key = (document.getElementById('storageKey') as HTMLInputElement).value; 137 | 138 | try { 139 | const { transport } = testEnvironment!; 140 | const message = { 141 | jsonrpc: "2.0" as const, 142 | method: "resource", 143 | id: 2, 144 | params: { 145 | uri: `storage://${key}`, 146 | key 147 | } 148 | }; 149 | 150 | transport.onMessage((response) => { 151 | if (response.result && response.result.contents) { 152 | output.textContent = response.result.contents[0].text; 153 | } else if (response.error) { 154 | output.textContent = `Error: ${response.error.message}`; 155 | } 156 | }); 157 | 158 | await transport.send(message); 159 | } catch (error) { 160 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 161 | } 162 | }); 163 | } 164 | 165 | describe('Test Utils', () => { 166 | test('setupTestEnvironment should create transport and server', async () => { 167 | const env = await setupTestEnvironment(); 168 | expect(env.transport).toBeInstanceOf(BrowserTransport); 169 | expect(env.server).toBeInstanceOf(McpServer); 170 | expect(env.transport.isConnected()).toBe(true); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /src/web/browser-transport.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; 2 | 3 | export interface JSONRPCMessage { 4 | jsonrpc: "2.0"; 5 | id: number; 6 | method: string; 7 | params: any; 8 | } 9 | 10 | export class BrowserTransport implements Transport { 11 | private callbacks: ((message: any) => void)[] = []; 12 | private started = false; 13 | private isTestMode = false; 14 | 15 | constructor(isTestMode = false) { 16 | this.isTestMode = isTestMode; 17 | } 18 | 19 | async start(): Promise { 20 | this.started = true; 21 | window.addEventListener("message", this.handleWindowMessage.bind(this)); 22 | console.log('Browser transport initialized'); 23 | } 24 | 25 | async stop(): Promise { 26 | this.started = false; 27 | window.removeEventListener("message", this.handleWindowMessage.bind(this)); 28 | this.callbacks = []; 29 | console.log('Browser transport stopped'); 30 | } 31 | 32 | async close(): Promise { 33 | await this.stop(); 34 | } 35 | 36 | isConnected(): boolean { 37 | return this.started; 38 | } 39 | 40 | onMessage(callback: (message: any) => void): () => void { 41 | this.callbacks.push(callback); 42 | return () => { 43 | const index = this.callbacks.indexOf(callback); 44 | if (index > -1) { 45 | this.callbacks.splice(index, 1); 46 | } 47 | }; 48 | } 49 | 50 | async send(message: any): Promise { 51 | if (!this.started) { 52 | throw new Error('Transport not connected'); 53 | } 54 | 55 | if (this.isTestMode) { 56 | // In test mode, just pass through the message 57 | window.postMessage({ 58 | type: "mcp-message", 59 | message 60 | }, "*"); 61 | this.callbacks.forEach(callback => callback(message)); 62 | return; 63 | } 64 | 65 | try { 66 | let response; 67 | if (message.method === 'tool') { 68 | const result = await this.handleToolMessage(message); 69 | response = { 70 | jsonrpc: "2.0", 71 | id: message.id, 72 | result 73 | }; 74 | } else if (message.method === 'resource') { 75 | const result = await this.handleResourceMessage(message); 76 | response = { 77 | jsonrpc: "2.0", 78 | id: message.id, 79 | result 80 | }; 81 | } else { 82 | throw new Error(`Unknown method: ${message.method}`); 83 | } 84 | 85 | // Notify all callbacks 86 | this.callbacks.forEach(callback => callback(response)); 87 | } catch (error) { 88 | const errorResponse = { 89 | jsonrpc: "2.0", 90 | id: message.id, 91 | error: { 92 | code: -32000, 93 | message: error instanceof Error ? error.message : 'Unknown error' 94 | } 95 | }; 96 | this.callbacks.forEach(callback => callback(errorResponse)); 97 | } 98 | } 99 | 100 | private handleWindowMessage(event: MessageEvent): void { 101 | if (event.data && event.data.type === "mcp-message") { 102 | const message = event.data.message; 103 | this.callbacks.forEach(callback => callback(message)); 104 | } 105 | } 106 | 107 | private async handleToolMessage(message: any) { 108 | const { name, params } = message.params; 109 | switch (name) { 110 | case 'calculate': { 111 | const { operation, a, b } = params; 112 | let result; 113 | switch (operation) { 114 | case 'add': 115 | result = a + b; 116 | break; 117 | case 'subtract': 118 | result = a - b; 119 | break; 120 | case 'multiply': 121 | result = a * b; 122 | break; 123 | case 'divide': 124 | if (b === 0) throw new Error('Division by zero'); 125 | result = a / b; 126 | break; 127 | default: 128 | throw new Error('Invalid operation'); 129 | } 130 | return { 131 | content: [{ type: "text", text: result.toString() }] 132 | }; 133 | } 134 | case 'set-storage': { 135 | const { key, value } = params; 136 | localStorage.setItem(key, value); 137 | return { 138 | content: [{ type: "text", text: 'Value stored successfully' }] 139 | }; 140 | } 141 | default: 142 | throw new Error(`Unknown tool: ${name}`); 143 | } 144 | } 145 | 146 | private async handleResourceMessage(message: any) { 147 | const { uri, key } = message.params; 148 | const value = localStorage.getItem(key); 149 | if (!value) { 150 | throw new Error('Key not found'); 151 | } 152 | return { 153 | contents: [{ 154 | uri, 155 | text: value 156 | }] 157 | }; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WASM MCP Server Demo 7 | 36 | 37 | 38 |

WASM MCP Server Demo

39 | 40 |
41 |

Calculator Tool

42 | 48 | 49 | 50 | 51 |
52 |
53 | 54 |
55 |

Storage Tool

56 | 57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/web/main.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { BrowserTransport } from './browser-transport'; 3 | import { createServer } from './server'; 4 | 5 | async function setupEnvironment() { 6 | const transport = new BrowserTransport(); 7 | await transport.start(); 8 | 9 | const server = createServer(); 10 | await server.connect(transport); 11 | 12 | return { server, transport }; 13 | } 14 | 15 | async function initializeCalculator(transport: BrowserTransport) { 16 | const calcButton = document.getElementById('calcButton'); 17 | const output = document.getElementById('calcOutput'); 18 | 19 | if (!calcButton || !output) { 20 | throw new Error('Calculator UI elements not found'); 21 | } 22 | 23 | calcButton.addEventListener('click', async () => { 24 | const operation = (document.getElementById('operation') as HTMLSelectElement).value; 25 | const a = parseFloat((document.getElementById('num1') as HTMLInputElement).value); 26 | const b = parseFloat((document.getElementById('num2') as HTMLInputElement).value); 27 | 28 | try { 29 | const message = { 30 | jsonrpc: "2.0" as const, 31 | method: "tool", 32 | id: 1, 33 | params: { 34 | name: "calculate", 35 | params: { 36 | operation, 37 | a, 38 | b 39 | } 40 | } 41 | }; 42 | 43 | transport.onMessage((response) => { 44 | if (response.result && response.result.content) { 45 | output.textContent = `Result: ${response.result.content[0].text}`; 46 | } else if (response.error) { 47 | output.textContent = `Error: ${response.error.message}`; 48 | } 49 | }); 50 | 51 | await transport.send(message); 52 | } catch (error) { 53 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 54 | } 55 | }); 56 | } 57 | 58 | async function initializeStorage(transport: BrowserTransport) { 59 | const setButton = document.getElementById('setStorageButton'); 60 | const getButton = document.getElementById('getStorageButton'); 61 | const output = document.getElementById('storageOutput'); 62 | 63 | if (!setButton || !getButton || !output) { 64 | throw new Error('Storage UI elements not found'); 65 | } 66 | 67 | setButton.addEventListener('click', async () => { 68 | const key = (document.getElementById('storageKey') as HTMLInputElement).value; 69 | const value = (document.getElementById('storageValue') as HTMLInputElement).value; 70 | 71 | try { 72 | const message = { 73 | jsonrpc: "2.0" as const, 74 | method: "tool", 75 | id: 1, 76 | params: { 77 | name: "set-storage", 78 | params: { 79 | key, 80 | value 81 | } 82 | } 83 | }; 84 | 85 | transport.onMessage((response) => { 86 | if (response.result && response.result.content) { 87 | output.textContent = response.result.content[0].text; 88 | } else if (response.error) { 89 | output.textContent = `Error: ${response.error.message}`; 90 | } 91 | }); 92 | 93 | await transport.send(message); 94 | } catch (error) { 95 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 96 | } 97 | }); 98 | 99 | getButton.addEventListener('click', async () => { 100 | const key = (document.getElementById('storageKey') as HTMLInputElement).value; 101 | 102 | try { 103 | const message = { 104 | jsonrpc: "2.0" as const, 105 | method: "resource", 106 | id: 2, 107 | params: { 108 | uri: `storage://${key}`, 109 | key 110 | } 111 | }; 112 | 113 | transport.onMessage((response) => { 114 | if (response.result && response.result.contents) { 115 | output.textContent = response.result.contents[0].text; 116 | } else if (response.error) { 117 | output.textContent = `Error: ${response.error.message}`; 118 | } 119 | }); 120 | 121 | await transport.send(message); 122 | } catch (error) { 123 | output.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; 124 | } 125 | }); 126 | } 127 | 128 | async function main() { 129 | const { transport } = await setupEnvironment(); 130 | await initializeCalculator(transport); 131 | await initializeStorage(transport); 132 | } 133 | 134 | main().catch(console.error); 135 | -------------------------------------------------------------------------------- /src/web/server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | const calculatorSchema = z.object({ 5 | operation: z.enum(['add', 'subtract', 'multiply', 'divide']), 6 | a: z.number(), 7 | b: z.number() 8 | }); 9 | 10 | const storageSchema = z.object({ 11 | key: z.string(), 12 | value: z.string() 13 | }); 14 | 15 | // In-memory storage 16 | const storage = new Map(); 17 | 18 | export function createServer(): McpServer { 19 | const server = new McpServer({ 20 | name: "WASM MCP Server", 21 | version: "1.0.0" 22 | }); 23 | 24 | // Register calculator tool 25 | server.tool( 26 | "calculate", 27 | calculatorSchema.shape, 28 | async (params) => { 29 | // Parse and validate inputs 30 | const result = calculatorSchema.safeParse(params); 31 | if (!result.success) { 32 | throw new Error('Invalid input parameters'); 33 | } 34 | 35 | const { operation, a, b } = result.data; 36 | let value: number; 37 | 38 | switch (operation) { 39 | case 'add': 40 | value = a + b; 41 | break; 42 | case 'subtract': 43 | value = a - b; 44 | break; 45 | case 'multiply': 46 | value = a * b; 47 | break; 48 | case 'divide': 49 | if (b === 0) throw new Error('Division by zero'); 50 | value = a / b; 51 | break; 52 | default: 53 | throw new Error('Invalid operation'); 54 | } 55 | 56 | return { 57 | content: [{ type: "text", text: value.toString() }] 58 | }; 59 | } 60 | ); 61 | 62 | // Register storage tool 63 | server.tool( 64 | "set-storage", 65 | storageSchema.shape, 66 | async (params) => { 67 | // Parse and validate inputs 68 | const result = storageSchema.safeParse(params); 69 | if (!result.success) { 70 | throw new Error('Invalid input parameters'); 71 | } 72 | 73 | const { key, value } = result.data; 74 | storage.set(key, value); 75 | 76 | return { 77 | content: [{ type: "text", text: 'Value stored successfully' }] 78 | }; 79 | } 80 | ); 81 | 82 | // Register storage resource 83 | server.resource( 84 | "storage", 85 | "storage://{key}", 86 | async (uri: URL, extra: any) => { 87 | const key = extra.key; 88 | if (!key) { 89 | throw new Error('Missing key parameter'); 90 | } 91 | 92 | const value = storage.get(key); 93 | if (!value) { 94 | return { 95 | contents: [{ 96 | uri: uri.toString(), 97 | text: 'Key not found' 98 | }] 99 | }; 100 | } 101 | 102 | return { 103 | contents: [{ 104 | uri: uri.toString(), 105 | text: value 106 | }] 107 | }; 108 | } 109 | ); 110 | 111 | return server; 112 | } 113 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "src/wasm"] 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | root: 'src/web', 5 | build: { 6 | outDir: '../../dist', 7 | emptyOutDir: true, 8 | }, 9 | server: { 10 | port: 3000, 11 | } 12 | }); 13 | --------------------------------------------------------------------------------