# Directory Structure ``` ├── .azdignore ├── .env.example ├── .github │ └── copilot-instructions.md ├── .gitignore ├── .nvmrc ├── .vscode │ └── tasks.json ├── azure.yaml ├── infra │ ├── abbreviations.json │ ├── core │ │ └── host │ │ ├── appservice.bicep │ │ └── appserviceplan.bicep │ ├── main.bicep │ └── main.parameters.json ├── mcp-config.json ├── package-lock.json ├── package.json ├── public │ └── test.html ├── README.md ├── server.js ├── test-client.js └── web.config ``` # Files -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- ``` 1 | 22 2 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Example environment variables for the MCP Server 2 | # Copy this file to .env and update the values as needed 3 | 4 | # Server Configuration 5 | PORT=8000 6 | NODE_ENV=development 7 | 8 | # CORS Configuration (optional - defaults to allow all origins in development) 9 | # ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com 10 | 11 | # Logging Level (optional - defaults to 'info') 12 | # LOG_LEVEL=debug 13 | ``` -------------------------------------------------------------------------------- /.azdignore: -------------------------------------------------------------------------------- ``` 1 | # Azure Developer CLI ignore file 2 | # Files and directories to ignore during deployment 3 | 4 | # Development files 5 | .env 6 | .env.local 7 | .env.development 8 | .env.test 9 | .env.production 10 | 11 | # Node.js 12 | node_modules/ 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage/ 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Dependency directories 31 | node_modules/ 32 | jspm_packages/ 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # Output of 'npm pack' 41 | *.tgz 42 | 43 | # Yarn Integrity file 44 | .yarn-integrity 45 | 46 | # dotenv environment variables file 47 | .env 48 | .env.test 49 | 50 | # parcel-bundler cache (https://parceljs.org/) 51 | .cache 52 | .parcel-cache 53 | 54 | # VS Code 55 | .vscode/ 56 | .history/ 57 | 58 | # Git 59 | .git/ 60 | .gitignore 61 | 62 | # Azure Dev CLI 63 | .azure/ 64 | 65 | # Development and testing 66 | test-client.js 67 | public/test.html 68 | start_server.ps1 69 | start_server.bat 70 | 71 | # Documentation (optional - remove if you want to include docs) 72 | README.md 73 | DEPLOY.md 74 | 75 | # Windows 76 | Thumbs.db 77 | ehthumbs.db 78 | Desktop.ini 79 | $RECYCLE.BIN/ 80 | 81 | # macOS 82 | .DS_Store 83 | .AppleDouble 84 | .LSOverride 85 | Icon 86 | ._* 87 | 88 | # Linux 89 | *~ 90 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Azure deployment 133 | .azure/ 134 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Node.js MCP Weather Server with Azure Deployment 2 | 3 | A Model Context Protocol (MCP) server built with Express.js and Node.js that provides weather information using the National Weather Service API. Ready for deployment to Azure App Service with Azure Developer CLI (azd). 4 | 5 | ## 🌟 Features 6 | 7 | - **Express.js Framework**: Fast, unopinionated web framework for Node.js 8 | - **MCP Protocol Compliance**: Full support for JSON-RPC 2.0 MCP protocol 9 | - **HTTP Transport**: HTTP-based communication for web connectivity 10 | - **Weather Tools**: 11 | - `get_alerts`: Get weather alerts for any US state 12 | - `get_forecast`: Get detailed weather forecast for any location 13 | - **Azure Ready**: Pre-configured for Azure App Service deployment 14 | - **Web Test Interface**: Built-in HTML interface for testing 15 | - **National Weather Service API**: Real-time weather data from official US government source 16 | 17 | ## 💻 Local Development 18 | 19 | ### Prerequisites 20 | - **Node.js 22+** (or Node.js 18+) 21 | - **npm** (Node Package Manager) 22 | 23 | ### Setup & Run 24 | 25 | 1. **Clone and install dependencies**: 26 | ```bash 27 | git clone <your-repo-url> 28 | cd remote-mcp-webapp-node 29 | npm install 30 | ``` 31 | 32 | 2. **Start the development server**: 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | 3. **Access the server**: 38 | - Server: http://localhost:8000 39 | - Health Check: http://localhost:8000/health 40 | - Test Interface: http://localhost:8000/test 41 | 42 | ## 🔌 Connect to the Local MCP Server 43 | 44 | ### Using VS Code - Copilot Agent Mode 45 | 46 | 1. **Add MCP Server** from command palette and add the URL to your running server's HTTP endpoint: 47 | ``` 48 | http://localhost:8000 49 | ``` 50 | 2. **List MCP Servers** from command palette and start the server 51 | 3. In Copilot chat agent mode, enter a prompt to trigger the tool: 52 | ``` 53 | What's the weather forecast for San Francisco? 54 | ``` 55 | 4. When prompted to run the tool, consent by clicking **Continue** 56 | 57 | ### Using MCP Inspector 58 | 59 | 1. In a **new terminal window**, install and run MCP Inspector: 60 | ```bash 61 | npx @modelcontextprotocol/inspector 62 | ``` 63 | 2. CTRL+click the URL displayed by the app (e.g. http://localhost:5173/#resources) 64 | 3. Set the transport type to `HTTP` 65 | 4. Set the URL to your running server's HTTP endpoint and **Connect**: 66 | ``` 67 | http://localhost:8000 68 | ``` 69 | 5. **List Tools**, click on a tool, and **Run Tool** 70 | 71 | ## 🚀 Quick Deploy to Azure 72 | 73 | ### Prerequisites 74 | - [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) 75 | - [Azure Developer CLI (azd)](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) 76 | - Active Azure subscription 77 | 78 | ### Deploy in 3 Commands 79 | 80 | ```bash 81 | # 1. Login to Azure 82 | azd auth login 83 | 84 | # 2. Initialize the project 85 | azd init 86 | 87 | # 3. Deploy to Azure 88 | azd up 89 | ``` 90 | 91 | After deployment, your MCP server will be available at: 92 | - **Health Check**: `https://<your-app>.azurewebsites.net/health` 93 | - **MCP Capabilities**: `https://<your-app>.azurewebsites.net/mcp/capabilities` 94 | - **Test Interface**: `https://<your-app>.azurewebsites.net/test` 95 | 96 | ## 🔌 Connect to the Remote MCP Server 97 | 98 | Follow the same guidance as above, but use your App Service URL instead. 99 | 100 | ## 🧪 Testing 101 | 102 | Visit `/test` endpoint for an interactive testing interface. 103 | 104 | ## 🌦️ Data Source 105 | 106 | This server uses the **National Weather Service (NWS) API**: 107 | - Real-time weather alerts and warnings 108 | - Detailed weather forecasts 109 | - Official US government weather data 110 | - No API key required 111 | - High reliability and accuracy ``` -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: remote-mcp-webapp-node 2 | metadata: 3 | template: [email protected] 4 | services: 5 | web: 6 | project: . 7 | language: js 8 | host: appservice 9 | ``` -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | } 14 | } 15 | } 16 | ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Start MCP Weather Server", 6 | "type": "shell", 7 | "command": "npm", 8 | "args": [ 9 | "run", 10 | "dev" 11 | ], 12 | "group": "build", 13 | "isBackground": true, 14 | "problemMatcher": [], 15 | "presentation": { 16 | "echo": true, 17 | "reveal": "always", 18 | "focus": false, 19 | "panel": "shared" 20 | } 21 | } 22 | ] 23 | } ``` -------------------------------------------------------------------------------- /mcp-config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "weather-mcp-server-local": { 4 | "transport": { 5 | "type": "http", 6 | "url": "http://localhost:8000/mcp/stream" 7 | }, 8 | "name": "Weather MCP Server (Local - Node.js)", 9 | "description": "MCP Server with weather forecast and alerts tools running locally on Node.js" 10 | }, 11 | "weather-mcp-server-azure": { 12 | "transport": { 13 | "type": "http", 14 | "url": "https://<APP-SERVICE-NAME>.azurewebsites.net/mcp/stream" 15 | }, 16 | "name": "Weather MCP Server (Azure - Node.js)", 17 | "description": "MCP Server with weather forecast and alerts tools hosted on Azure with Node.js" 18 | } 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "remote-mcp-webapp-node", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "description": "A Model Context Protocol (MCP) server built with Express.js that provides weather information using the National Weather Service API", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "node test-client.js", 10 | "test:azure": "node test-client.js --url=https://<APP-SERVICE-NAME>.azurewebsites.net", 11 | "build": "echo 'No build step required for this Node.js app'" 12 | }, 13 | "keywords": [ 14 | "mcp", 15 | "model-context-protocol", 16 | "weather", 17 | "api", 18 | "express", 19 | "nodejs", 20 | "jsonrpc" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "engines": { 25 | "node": ">=22.0.0" 26 | }, 27 | "dependencies": { 28 | "axios": "^1.9.0", 29 | "cors": "^2.8.5", 30 | "dotenv": "^16.5.0", 31 | "express": "^5.1.0", 32 | "helmet": "^8.1.0", 33 | "uuid": "^11.1.0" 34 | }, 35 | "devDependencies": { 36 | "nodemon": "^3.1.10" 37 | } 38 | } 39 | ``` -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <configuration> 3 | <system.webServer> 4 | <handlers> 5 | <add name="iisnode" path="server.js" verb="*" modules="iisnode"/> 6 | </handlers> 7 | <rewrite> 8 | <rules> 9 | <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true"> 10 | <match url="^server.js\/debug[\/]?" /> 11 | </rule> 12 | <rule name="StaticContent"> 13 | <action type="Rewrite" url="public{REQUEST_URI}"/> 14 | </rule> 15 | <rule name="DynamicContent"> 16 | <conditions> 17 | <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/> 18 | </conditions> 19 | <action type="Rewrite" url="server.js"/> 20 | </rule> 21 | </rules> 22 | </rewrite> 23 | <security> 24 | <requestFiltering> 25 | <hiddenSegments> 26 | <remove segment="bin"/> 27 | </hiddenSegments> 28 | </requestFiltering> 29 | </security> 30 | <httpErrors existingResponse="PassThrough" /> 31 | <iisnode watchedFiles="web.config;*.js"/> 32 | </system.webServer> 33 | </configuration> 34 | ``` -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file --> 2 | 3 | # MCP Server Development Guidelines 4 | 5 | This is an MCP (Model Context Protocol) server project built with Express.js and Node.js. 6 | 7 | ## Key Context 8 | 9 | - This project implements the MCP protocol using JSON-RPC 2.0 10 | - The server provides weather-related tools using the National Weather Service API 11 | - Main endpoints follow MCP specification for tools, resources, and capabilities 12 | - Use Express.js patterns and Node.js best practices 13 | - Weather data comes from official US government APIs (no API key required) 14 | 15 | ## Code Style Guidelines 16 | 17 | - Use modern JavaScript (ES6+) features where appropriate 18 | - Follow Express.js conventions for routing and middleware 19 | - Implement proper error handling with try-catch blocks 20 | - Use consistent JSON-RPC 2.0 response formats 21 | - Include comprehensive logging for debugging 22 | - Maintain backward compatibility with the MCP protocol 23 | 24 | ## Architecture Notes 25 | 26 | - `server.js` contains the main Express application and MCP protocol implementation 27 | - Weather tools are implemented as async functions that call NWS APIs 28 | - HTTP transport is used for MCP Inspector connectivity 29 | - Web test interface provides manual testing capabilities 30 | 31 | ## Testing 32 | 33 | - Use the built-in test client (`test-client.js`) for protocol testing 34 | - Web interface (`/test`) for manual verification 35 | - Ensure all MCP methods return proper JSON-RPC 2.0 responses 36 | 37 | You can find more info and examples at https://modelcontextprotocol.io/llms-full.txt 38 | ``` -------------------------------------------------------------------------------- /test-client.js: -------------------------------------------------------------------------------- ```javascript 1 | const axios = require('axios'); 2 | 3 | class MCPTestClient { 4 | constructor(baseUrl = 'http://localhost:8000') { 5 | this.baseUrl = baseUrl; 6 | this.mcpEndpoint = `${baseUrl}/mcp/stream`; 7 | } 8 | 9 | async makeRequest(method, params = {}, id = null) { 10 | const request = { 11 | jsonrpc: '2.0', 12 | method, 13 | params, 14 | id: id || Math.floor(Math.random() * 1000) 15 | }; 16 | 17 | try { 18 | console.log(`\n🔄 Sending request: ${method}`); 19 | console.log('Request:', JSON.stringify(request, null, 2)); 20 | 21 | const response = await axios.post(this.mcpEndpoint, request); 22 | 23 | console.log('✅ Response received:'); 24 | console.log(JSON.stringify(response.data, null, 2)); 25 | 26 | return response.data; 27 | } catch (error) { 28 | console.error('❌ Error:', error.message); 29 | if (error.response) { 30 | console.error('Response data:', error.response.data); 31 | } 32 | throw error; 33 | } 34 | } 35 | 36 | async testInitialize() { 37 | console.log('\n=== Testing Initialize ==='); 38 | return await this.makeRequest('initialize', {}); 39 | } 40 | 41 | async testListTools() { 42 | console.log('\n=== Testing List Tools ==='); 43 | return await this.makeRequest('tools/list'); 44 | } 45 | 46 | async testGetAlerts(state = 'CA') { 47 | console.log('\n=== Testing Weather Alerts ==='); 48 | return await this.makeRequest('tools/call', { 49 | name: 'get_alerts', 50 | arguments: { state } 51 | }); 52 | } 53 | 54 | async testGetForecast(latitude = 37.7749, longitude = -122.4194) { 55 | console.log('\n=== Testing Weather Forecast ==='); 56 | return await this.makeRequest('tools/call', { 57 | name: 'get_forecast', 58 | arguments: { latitude, longitude } 59 | }); 60 | } 61 | 62 | async testListResources() { 63 | console.log('\n=== Testing List Resources ==='); 64 | return await this.makeRequest('resources/list'); 65 | } 66 | 67 | async testReadResource(uri = 'mcp://server/sample') { 68 | console.log('\n=== Testing Read Resource ==='); 69 | return await this.makeRequest('resources/read', { uri }); 70 | } 71 | 72 | async checkHealth() { 73 | console.log('\n=== Checking Server Health ==='); 74 | try { 75 | const response = await axios.get(`${this.baseUrl}/health`); 76 | console.log('✅ Health check passed:'); 77 | console.log(JSON.stringify(response.data, null, 2)); 78 | return response.data; 79 | } catch (error) { 80 | console.error('❌ Health check failed:', error.message); 81 | throw error; 82 | } 83 | } 84 | 85 | async runAllTests() { 86 | console.log('🚀 Starting MCP Server Tests'); 87 | console.log('================================'); 88 | 89 | try { 90 | // Check server health first 91 | await this.checkHealth(); 92 | 93 | // Test MCP protocol methods 94 | await this.testInitialize(); 95 | await this.testListTools(); 96 | await this.testListResources(); 97 | await this.testReadResource(); 98 | 99 | // Test weather tools 100 | await this.testGetAlerts('CA'); 101 | await this.testGetForecast(37.7749, -122.4194); // San Francisco 102 | 103 | console.log('\n🎉 All tests completed successfully!'); 104 | } catch (error) { 105 | console.error('\n💥 Test suite failed:', error.message); 106 | process.exit(1); 107 | } 108 | } 109 | } 110 | 111 | // Run tests if this file is executed directly 112 | if (require.main === module) { 113 | const client = new MCPTestClient(); 114 | 115 | // Parse command line arguments 116 | const args = process.argv.slice(2); 117 | const serverUrl = args.find(arg => arg.startsWith('--url='))?.split('=')[1] || 'http://localhost:8000'; 118 | 119 | if (serverUrl !== 'http://localhost:8000') { 120 | client.baseUrl = serverUrl; 121 | client.mcpEndpoint = `${serverUrl}/mcp/stream`; 122 | console.log(`🌐 Testing server at: ${serverUrl}`); 123 | } 124 | 125 | client.runAllTests(); 126 | } 127 | 128 | module.exports = MCPTestClient; 129 | ``` -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des-", 20 | "computeDisks": "disk-", 21 | "computeDisksOs": "osdisk-", 22 | "computeGalleries": "gal-", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm-", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci-", 27 | "containerRegistryRegistries": "cr-", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla-", 32 | "dataLakeStoreAccounts": "dls-", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopic": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck-", 56 | "kustoClusters": "dec-", 57 | "loadTesting": "lt-", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf-", 73 | "networkFirewallPoliciesRuleGroups": "wafrg-", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr-", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabase": "sqldb-", 120 | "sqlServersFirewallRules": "sqlfw-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "streamAnalyticsCluster": "asa-", 124 | "synapseWorkspaces": "syn-", 125 | "timeSeriesInsightsEnvironments": "tsi-", 126 | "webServerFarms": "plan-", 127 | "webSitesAppService": "app-", 128 | "webSitesAppServiceEnvironment": "ase-", 129 | "webSitesFunctions": "func-" 130 | } 131 | ``` -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- ```javascript 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const helmet = require('helmet'); 4 | const axios = require('axios'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const { v4: uuidv4 } = require('uuid'); 8 | 9 | const app = express(); 10 | const PORT = process.env.PORT || 8000; 11 | 12 | // Middleware 13 | app.use(helmet()); 14 | app.use(cors()); 15 | app.use(express.json()); 16 | app.use(express.static('public')); 17 | 18 | // MCP Server Implementation 19 | class MCPServer { 20 | constructor() { 21 | this.tools = [ 22 | { 23 | name: 'get_alerts', 24 | description: 'Get weather alerts for a US state.', 25 | inputSchema: { 26 | type: 'object', 27 | properties: { 28 | state: { 29 | type: 'string', 30 | description: 'Two-letter US state code (e.g. CA, NY)' 31 | } 32 | }, 33 | required: ['state'] 34 | } 35 | }, 36 | { 37 | name: 'get_forecast', 38 | description: 'Get weather forecast for a location.', 39 | inputSchema: { 40 | type: 'object', 41 | properties: { 42 | latitude: { 43 | type: 'number', 44 | description: 'Latitude of the location' 45 | }, 46 | longitude: { 47 | type: 'number', 48 | description: 'Longitude of the location' 49 | } 50 | }, 51 | required: ['latitude', 'longitude'] 52 | } 53 | } 54 | ]; 55 | 56 | this.resources = [ 57 | { 58 | uri: 'mcp://server/sample', 59 | name: 'Sample Resource', 60 | description: 'Sample resource for demonstration', 61 | mimeType: 'text/plain' 62 | } 63 | ]; 64 | } 65 | 66 | async handleRequest(request) { 67 | const { jsonrpc, method, params, id } = request; 68 | 69 | if (jsonrpc !== '2.0') { 70 | return this.createErrorResponse(id, -32600, 'Invalid Request'); 71 | } 72 | 73 | try { 74 | switch (method) { 75 | case 'initialize': 76 | return this.handleInitialize(id, params); 77 | case 'tools/list': 78 | return this.handleToolsList(id); 79 | case 'tools/call': 80 | return await this.handleToolsCall(id, params); 81 | case 'resources/list': 82 | return this.handleResourcesList(id); 83 | case 'resources/read': 84 | return this.handleResourcesRead(id, params); 85 | default: 86 | return this.createErrorResponse(id, -32601, 'Method not found'); 87 | } 88 | } catch (error) { 89 | console.error('Error handling request:', error); 90 | return this.createErrorResponse(id, -32603, 'Internal error'); 91 | } 92 | } 93 | 94 | handleInitialize(id, params) { 95 | return { 96 | jsonrpc: '2.0', 97 | id, 98 | result: { 99 | protocolVersion: '2024-11-05', 100 | capabilities: { 101 | tools: {}, 102 | resources: {} 103 | }, 104 | serverInfo: { 105 | name: 'Weather MCP Server (Node.js)', 106 | version: '1.0.0' 107 | } 108 | } 109 | }; 110 | } 111 | 112 | handleToolsList(id) { 113 | return { 114 | jsonrpc: '2.0', 115 | id, 116 | result: { 117 | tools: this.tools 118 | } 119 | }; 120 | } 121 | 122 | async handleToolsCall(id, params) { 123 | const { name, arguments: args } = params; 124 | 125 | try { 126 | let result; 127 | switch (name) { 128 | case 'get_alerts': 129 | result = await this.getAlerts(args.state); 130 | break; 131 | case 'get_forecast': 132 | result = await this.getForecast(args.latitude, args.longitude); 133 | break; 134 | default: 135 | return this.createErrorResponse(id, -32602, 'Invalid tool name'); 136 | } 137 | 138 | return { 139 | jsonrpc: '2.0', 140 | id, 141 | result: { 142 | content: [ 143 | { 144 | type: 'text', 145 | text: result 146 | } 147 | ] 148 | } 149 | }; 150 | } catch (error) { 151 | return this.createErrorResponse(id, -32603, `Tool execution error: ${error.message}`); 152 | } 153 | } 154 | 155 | handleResourcesList(id) { 156 | return { 157 | jsonrpc: '2.0', 158 | id, 159 | result: { 160 | resources: this.resources 161 | } 162 | }; 163 | } 164 | 165 | handleResourcesRead(id, params) { 166 | const { uri } = params; 167 | 168 | if (uri === 'mcp://server/sample') { 169 | return { 170 | jsonrpc: '2.0', 171 | id, 172 | result: { 173 | contents: [ 174 | { 175 | uri, 176 | mimeType: 'text/plain', 177 | text: 'This is a sample resource from the MCP server.' 178 | } 179 | ] 180 | } 181 | }; 182 | } 183 | 184 | return this.createErrorResponse(id, -32602, 'Resource not found'); 185 | } 186 | 187 | async getAlerts(state) { 188 | try { 189 | const response = await axios.get(`https://api.weather.gov/alerts/active?area=${state.toUpperCase()}`); 190 | const alerts = response.data.features; 191 | 192 | if (alerts.length === 0) { 193 | return `No active weather alerts for ${state.toUpperCase()}.`; 194 | } 195 | 196 | let alertsText = `Active weather alerts for ${state.toUpperCase()}:\n\n`; 197 | alerts.forEach((alert, index) => { 198 | const properties = alert.properties; 199 | alertsText += `${index + 1}. ${properties.headline}\n`; 200 | alertsText += ` Severity: ${properties.severity}\n`; 201 | alertsText += ` Urgency: ${properties.urgency}\n`; 202 | alertsText += ` Event: ${properties.event}\n`; 203 | alertsText += ` Description: ${properties.description}\n`; 204 | if (properties.instruction) { 205 | alertsText += ` Instructions: ${properties.instruction}\n`; 206 | } 207 | alertsText += '\n'; 208 | }); 209 | 210 | return alertsText; 211 | } catch (error) { 212 | throw new Error(`Failed to fetch weather alerts: ${error.message}`); 213 | } 214 | } 215 | 216 | async getForecast(latitude, longitude) { 217 | try { 218 | // First, get the grid point 219 | const pointResponse = await axios.get(`https://api.weather.gov/points/${latitude},${longitude}`); 220 | const forecastUrl = pointResponse.data.properties.forecast; 221 | 222 | // Then get the forecast 223 | const forecastResponse = await axios.get(forecastUrl); 224 | const periods = forecastResponse.data.properties.periods; 225 | 226 | let forecastText = `Weather forecast for ${latitude}, ${longitude}:\n\n`; 227 | periods.slice(0, 10).forEach((period, index) => { 228 | forecastText += `${period.name}:\n`; 229 | forecastText += ` Temperature: ${period.temperature}°${period.temperatureUnit}\n`; 230 | forecastText += ` Wind: ${period.windSpeed} ${period.windDirection}\n`; 231 | forecastText += ` Forecast: ${period.detailedForecast}\n\n`; 232 | }); 233 | 234 | return forecastText; 235 | } catch (error) { 236 | throw new Error(`Failed to fetch weather forecast: ${error.message}`); 237 | } 238 | } 239 | 240 | createErrorResponse(id, code, message) { 241 | return { 242 | jsonrpc: '2.0', 243 | id, 244 | error: { 245 | code, 246 | message 247 | } 248 | }; 249 | } 250 | } 251 | 252 | const mcpServer = new MCPServer(); 253 | 254 | // Routes 255 | app.get('/health', (req, res) => { 256 | res.json({ status: 'healthy', timestamp: new Date().toISOString() }); 257 | }); 258 | 259 | app.get('/mcp/capabilities', (req, res) => { 260 | res.json({ 261 | protocolVersion: '2024-11-05', 262 | capabilities: { 263 | tools: {}, 264 | resources: {} 265 | }, 266 | serverInfo: { 267 | name: 'Weather MCP Server (Node.js)', 268 | version: '1.0.0' 269 | } 270 | }); 271 | }); 272 | 273 | // Main MCP endpoint with streamable HTTP 274 | app.post('/mcp/stream', async (req, res) => { 275 | try { 276 | const response = await mcpServer.handleRequest(req.body); 277 | res.json(response); 278 | } catch (error) { 279 | console.error('MCP Stream Error:', error); 280 | res.status(500).json({ 281 | jsonrpc: '2.0', 282 | id: req.body.id || null, 283 | error: { 284 | code: -32603, 285 | message: 'Internal error' 286 | } 287 | }); 288 | } 289 | }); 290 | 291 | // Legacy MCP endpoint 292 | app.post('/mcp', async (req, res) => { 293 | try { 294 | const response = await mcpServer.handleRequest(req.body); 295 | res.json(response); 296 | } catch (error) { 297 | console.error('MCP Error:', error); 298 | res.status(500).json({ 299 | jsonrpc: '2.0', 300 | id: req.body.id || null, 301 | error: { 302 | code: -32603, 303 | message: 'Internal error' 304 | } 305 | }); 306 | } 307 | }); 308 | 309 | // Web test interface 310 | app.get('/test', (req, res) => { 311 | res.sendFile(path.join(__dirname, 'public', 'test.html')); 312 | }); 313 | 314 | // Start server 315 | app.listen(PORT, '0.0.0.0', () => { 316 | console.log(`MCP Weather Server running on http://localhost:${PORT}`); 317 | console.log(`Health check: http://localhost:${PORT}/health`); 318 | console.log(`MCP Endpoint: http://localhost:${PORT}/mcp/stream`); 319 | console.log(`Web Test Interface: http://localhost:${PORT}/test`); 320 | console.log(`API Documentation: http://localhost:${PORT}/mcp/capabilities`); 321 | }); 322 | ``` -------------------------------------------------------------------------------- /public/test.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <title>MCP Weather Server - Test Interface</title> 7 | <style> 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 | max-width: 1200px; 11 | margin: 0 auto; 12 | padding: 20px; 13 | background-color: #f5f5f5; 14 | } 15 | .container { 16 | background: white; 17 | border-radius: 8px; 18 | padding: 30px; 19 | box-shadow: 0 2px 10px rgba(0,0,0,0.1); 20 | } 21 | h1 { 22 | color: #2563eb; 23 | margin-bottom: 10px; 24 | } 25 | .subtitle { 26 | color: #6b7280; 27 | margin-bottom: 30px; 28 | } 29 | .section { 30 | margin-bottom: 30px; 31 | padding: 20px; 32 | border: 1px solid #e5e7eb; 33 | border-radius: 6px; 34 | background-color: #f9fafb; 35 | } 36 | .section h3 { 37 | margin-top: 0; 38 | color: #374151; 39 | } 40 | .form-group { 41 | margin-bottom: 15px; 42 | } 43 | label { 44 | display: block; 45 | margin-bottom: 5px; 46 | font-weight: 500; 47 | color: #374151; 48 | } 49 | input, select, textarea { 50 | width: 100%; 51 | padding: 8px 12px; 52 | border: 1px solid #d1d5db; 53 | border-radius: 4px; 54 | font-size: 14px; 55 | box-sizing: border-box; 56 | } 57 | button { 58 | background-color: #2563eb; 59 | color: white; 60 | padding: 10px 20px; 61 | border: none; 62 | border-radius: 4px; 63 | cursor: pointer; 64 | font-size: 14px; 65 | margin-right: 10px; 66 | margin-bottom: 10px; 67 | } 68 | button:hover { 69 | background-color: #1d4ed8; 70 | } 71 | .response { 72 | margin-top: 20px; 73 | padding: 15px; 74 | background-color: #f3f4f6; 75 | border-radius: 4px; 76 | border-left: 4px solid #2563eb; 77 | white-space: pre-wrap; 78 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 79 | font-size: 13px; 80 | line-height: 1.4; 81 | } 82 | .error { 83 | border-left-color: #ef4444; 84 | background-color: #fef2f2; 85 | color: #991b1b; 86 | } 87 | .success { 88 | border-left-color: #10b981; 89 | background-color: #f0fdf4; 90 | color: #065f46; 91 | } 92 | .json-input { 93 | height: 120px; 94 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 95 | font-size: 13px; 96 | } 97 | </style> 98 | </head> 99 | <body> 100 | <div class="container"> 101 | <h1>🌤️ MCP Weather Server Test Interface</h1> 102 | <p class="subtitle">Test the Model Context Protocol (MCP) server with weather tools</p> 103 | 104 | <div class="section"> 105 | <h3>🔧 Server Status</h3> 106 | <button onclick="checkHealth()">Check Health</button> 107 | <button onclick="getCapabilities()">Get Capabilities</button> 108 | <button onclick="listTools()">List Tools</button> 109 | <div id="statusResponse" class="response" style="display: none;"></div> 110 | </div> 111 | 112 | <div class="section"> 113 | <h3>🚨 Weather Alerts</h3> 114 | <div class="form-group"> 115 | <label for="stateSelect">Select State:</label> 116 | <select id="stateSelect"> 117 | <option value="CA">California (CA)</option> 118 | <option value="NY">New York (NY)</option> 119 | <option value="TX">Texas (TX)</option> 120 | <option value="FL">Florida (FL)</option> 121 | <option value="WA">Washington (WA)</option> 122 | <option value="IL">Illinois (IL)</option> 123 | <option value="PA">Pennsylvania (PA)</option> 124 | <option value="OH">Ohio (OH)</option> 125 | <option value="GA">Georgia (GA)</option> 126 | <option value="NC">North Carolina (NC)</option> 127 | </select> 128 | </div> 129 | <button onclick="getAlerts()">Get Weather Alerts</button> 130 | <div id="alertsResponse" class="response" style="display: none;"></div> 131 | </div> 132 | 133 | <div class="section"> 134 | <h3>🌡️ Weather Forecast</h3> 135 | <div class="form-group"> 136 | <label for="latitude">Latitude:</label> 137 | <input type="number" id="latitude" value="37.7749" step="any" placeholder="e.g., 37.7749"> 138 | </div> 139 | <div class="form-group"> 140 | <label for="longitude">Longitude:</label> 141 | <input type="number" id="longitude" value="-122.4194" step="any" placeholder="e.g., -122.4194"> 142 | </div> 143 | <button onclick="getForecast()">Get Weather Forecast</button> 144 | <div id="forecastResponse" class="response" style="display: none;"></div> 145 | </div> 146 | 147 | <div class="section"> 148 | <h3>🔍 Custom MCP Request</h3> 149 | <div class="form-group"> 150 | <label for="customRequest">JSON-RPC Request:</label> 151 | <textarea id="customRequest" class="json-input" placeholder='{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'></textarea> 152 | </div> 153 | <button onclick="sendCustomRequest()">Send Request</button> 154 | <div id="customResponse" class="response" style="display: none;"></div> 155 | </div> 156 | </div> 157 | 158 | <script> 159 | async function makeRequest(url, data) { 160 | try { 161 | const response = await fetch(url, { 162 | method: 'POST', 163 | headers: { 164 | 'Content-Type': 'application/json', 165 | }, 166 | body: JSON.stringify(data) 167 | }); 168 | return await response.json(); 169 | } catch (error) { 170 | throw new Error(`Network error: ${error.message}`); 171 | } 172 | } 173 | 174 | async function makeGetRequest(url) { 175 | try { 176 | const response = await fetch(url); 177 | return await response.json(); 178 | } catch (error) { 179 | throw new Error(`Network error: ${error.message}`); 180 | } 181 | } 182 | 183 | function showResponse(elementId, data, isError = false) { 184 | const element = document.getElementById(elementId); 185 | element.style.display = 'block'; 186 | element.className = `response ${isError ? 'error' : 'success'}`; 187 | element.textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2); 188 | } 189 | 190 | async function checkHealth() { 191 | try { 192 | const data = await makeGetRequest('/health'); 193 | showResponse('statusResponse', data); 194 | } catch (error) { 195 | showResponse('statusResponse', error.message, true); 196 | } 197 | } 198 | 199 | async function getCapabilities() { 200 | try { 201 | const data = await makeGetRequest('/mcp/capabilities'); 202 | showResponse('statusResponse', data); 203 | } catch (error) { 204 | showResponse('statusResponse', error.message, true); 205 | } 206 | } 207 | 208 | async function listTools() { 209 | try { 210 | const request = { 211 | jsonrpc: "2.0", 212 | method: "tools/list", 213 | id: 1 214 | }; 215 | const data = await makeRequest('/mcp/stream', request); 216 | showResponse('statusResponse', data); 217 | } catch (error) { 218 | showResponse('statusResponse', error.message, true); 219 | } 220 | } 221 | 222 | async function getAlerts() { 223 | try { 224 | const state = document.getElementById('stateSelect').value; 225 | const request = { 226 | jsonrpc: "2.0", 227 | method: "tools/call", 228 | params: { 229 | name: "get_alerts", 230 | arguments: { state: state } 231 | }, 232 | id: 2 233 | }; 234 | const data = await makeRequest('/mcp/stream', request); 235 | showResponse('alertsResponse', data); 236 | } catch (error) { 237 | showResponse('alertsResponse', error.message, true); 238 | } 239 | } 240 | 241 | async function getForecast() { 242 | try { 243 | const latitude = parseFloat(document.getElementById('latitude').value); 244 | const longitude = parseFloat(document.getElementById('longitude').value); 245 | 246 | if (isNaN(latitude) || isNaN(longitude)) { 247 | throw new Error('Please enter valid latitude and longitude values'); 248 | } 249 | 250 | const request = { 251 | jsonrpc: "2.0", 252 | method: "tools/call", 253 | params: { 254 | name: "get_forecast", 255 | arguments: { latitude: latitude, longitude: longitude } 256 | }, 257 | id: 3 258 | }; 259 | const data = await makeRequest('/mcp/stream', request); 260 | showResponse('forecastResponse', data); 261 | } catch (error) { 262 | showResponse('forecastResponse', error.message, true); 263 | } 264 | } 265 | 266 | async function sendCustomRequest() { 267 | try { 268 | const requestText = document.getElementById('customRequest').value; 269 | if (!requestText.trim()) { 270 | throw new Error('Please enter a JSON-RPC request'); 271 | } 272 | 273 | const request = JSON.parse(requestText); 274 | const data = await makeRequest('/mcp/stream', request); 275 | showResponse('customResponse', data); 276 | } catch (error) { 277 | showResponse('customResponse', error.message, true); 278 | } 279 | } 280 | 281 | // Load initial status on page load 282 | window.onload = function() { 283 | checkHealth(); 284 | }; 285 | </script> 286 | </body> 287 | </html> 288 | ```