GraphQL MCP Server
by ctkadvisors
Verified
- graphql-mcp
- docs
# Local Development Guide for Lambda-MCP
This guide provides detailed instructions for developing and testing Lambda-MCP servers locally before deploying them to AWS.
## Setting Up Your Local Environment
### Prerequisites
- Node.js 18 or later
- npm or yarn
- Git (for version control)
- Visual Studio Code (recommended) or another IDE
### Initial Setup
1. Clone the repository (or create a new project):
```bash
git clone https://github.com/yourusername/lambda-mcp.git
cd lambda-mcp
```
2. Install dependencies:
```bash
npm install
```
3. Install development utilities:
```bash
npm install --save-dev nodemon ts-node concurrently express @types/express
```
## Setting Up Local Server
The Lambda function normally runs in AWS, triggered by API Gateway. For local development, we'll create an Express server that simulates this environment.
### Create a Local Development Server
Create a file at `scripts/local-server.ts`:
```typescript
import express from 'express';
import * as path from 'path';
import { handler as helloWorldHandler } from '../src/examples/hello-world';
// Create Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON
app.use(express.json());
// Middleware to log requests
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// Endpoint for Hello World MCP server
app.post('/mcp/hello-world', async (req, res) => {
try {
// Create API Gateway event
const event = {
body: JSON.stringify(req.body),
headers: req.headers as Record<string, string>,
httpMethod: 'POST',
path: '/mcp/hello-world',
queryStringParameters: req.query as Record<string, string>
};
// Call the Lambda handler
const result = await helloWorldHandler(event as any, {} as any);
// Set status code and headers
res.status(result.statusCode);
if (result.headers) {
Object.entries(result.headers).forEach(([key, value]) => {
res.setHeader(key, value as string);
});
}
// Send response body
res.send(result.body);
} catch (error) {
console.error('Error handling request:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Add static file serving for testing UI (optional)
app.use(express.static(path.join(__dirname, '../public')));
// Start the server
app.listen(PORT, () => {
console.log(`MCP local development server running at http://localhost:${PORT}`);
console.log(`Hello World MCP endpoint: http://localhost:${PORT}/mcp/hello-world`);
});
```
### Update package.json Scripts
Add these scripts to your `package.json`:
```json
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"dev": "concurrently \"npm run watch\" \"nodemon dist/scripts/local-server.js\"",
"dev:ts": "ts-node scripts/local-server.ts",
"test": "jest"
}
```
## Creating a Testing UI
For easier testing, create a simple HTML interface to interact with your MCP server.
### Create HTML Testing Interface
Create a file at `public/index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lambda-MCP Tester</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
gap: 20px;
}
.panel {
flex: 1;
border: 1px solid #ccc;
border-radius: 4px;
padding: 15px;
}
textarea {
width: 100%;
height: 300px;
font-family: monospace;
margin-bottom: 10px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
.template-button {
background-color: #2196F3;
}
.history {
margin-top: 20px;
}
.history-item {
padding: 10px;
margin-bottom: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Lambda-MCP Tester</h1>
<div class="container">
<div class="panel">
<h2>Request</h2>
<select id="template-select">
<option value="">Select a template...</option>
<option value="initialize">initialize</option>
<option value="tool-list">tool.list</option>
<option value="hello-world">hello_world tool</option>
<option value="current-time">current_time tool</option>
</select>
<button class="template-button" id="load-template">Load Template</button>
<textarea id="request-body">{
"jsonrpc": "2.0",
"id": "req-1",
"method": "initialize",
"params": {
"client": {
"name": "Browser Tester",
"version": "1.0.0"
}
}
}</textarea>
<button id="send-request">Send Request</button>
</div>
<div class="panel">
<h2>Response</h2>
<textarea id="response-body" readonly></textarea>
<div>
<span>Status: </span><span id="status-code">-</span>
</div>
</div>
</div>
<div class="history">
<h2>History</h2>
<div id="history-container"></div>
</div>
<script>
const templates = {
initialize: {
jsonrpc: "2.0",
id: "req-1",
method: "initialize",
params: {
client: {
name: "Browser Tester",
version: "1.0.0"
}
}
},
"tool-list": {
jsonrpc: "2.0",
id: "req-2",
method: "tool.list",
params: {}
},
"hello-world": {
jsonrpc: "2.0",
id: "req-3",
method: "tool.call",
params: {
id: "call-1",
name: "hello_world",
arguments: {
name: "User",
language: "english"
}
}
},
"current-time": {
jsonrpc: "2.0",
id: "req-4",
method: "tool.call",
params: {
id: "call-2",
name: "current_time",
arguments: {
format: "iso"
}
}
}
};
document.getElementById('load-template').addEventListener('click', () => {
const template = document.getElementById('template-select').value;
if (template && templates[template]) {
document.getElementById('request-body').value = JSON.stringify(templates[template], null, 2);
}
});
document.getElementById('send-request').addEventListener('click', async () => {
const requestBody = document.getElementById('request-body').value;
const responseBodyEl = document.getElementById('response-body');
const statusCodeEl = document.getElementById('status-code');
try {
const response = await fetch('/mcp/hello-world', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: requestBody
});
statusCodeEl.textContent = response.status;
const responseData = await response.json();
responseBodyEl.value = JSON.stringify(responseData, null, 2);
// Add to history
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
const timestamp = new Date().toISOString();
const requestObj = JSON.parse(requestBody);
const method = requestObj.method;
historyItem.innerHTML = `
<div><strong>Time:</strong> ${timestamp}</div>
<div><strong>Method:</strong> ${method}</div>
<div><strong>Status:</strong> ${response.status}</div>
<details>
<summary>Request</summary>
<pre>${JSON.stringify(JSON.parse(requestBody), null, 2)}</pre>
</details>
<details>
<summary>Response</summary>
<pre>${JSON.stringify(responseData, null, 2)}</pre>
</details>
`;
document.getElementById('history-container').prepend(historyItem);
} catch (error) {
responseBodyEl.value = `Error: ${error.message}`;
statusCodeEl.textContent = 'Error';
}
});
</script>
</body>
</html>
```
## Setting Up VS Code Debugging
To enable debugging in VS Code, create a `.vscode/launch.json` file:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Local Server",
"program": "${workspaceFolder}/scripts/local-server.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true,
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug TS-Node",
"runtimeExecutable": "node",
"runtimeArgs": [
"--require", "ts-node/register",
"${workspaceFolder}/scripts/local-server.ts"
],
"console": "integratedTerminal",
"sourceMaps": true
}
]
}
```
## Testing with the Claude Desktop MCP Client
To test your locally running MCP server with Claude Desktop:
1. Expose your local server with a tool like ngrok:
```bash
npm install -g ngrok
ngrok http 3000
```
2. Get the public URL from ngrok (e.g., `https://abcd1234.ngrok.io`).
3. Configure Claude Desktop to use this URL for your MCP server:
- Open Claude Desktop settings
- Add a new MCP server with the URL: `https://abcd1234.ngrok.io/mcp/hello-world`
## Creating a Mock MCP Client
For automated testing, create a mock MCP client at `scripts/mock-mcp-client.ts`:
```typescript
import axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
const MCP_URL = process.env.MCP_URL || 'http://localhost:3000/mcp/hello-world';
async function sendRequest(body: any) {
console.log(`Sending request to ${MCP_URL}:`, JSON.stringify(body, null, 2));
try {
const response = await axios.post(MCP_URL, body, {
headers: {
'Content-Type': 'application/json',
},
});
console.log('Response:', JSON.stringify(response.data, null, 2));
return response.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
throw error;
}
}
async function runConversation() {
// 1. Initialize
const initResult = await sendRequest({
jsonrpc: '2.0',
id: 'req-1',
method: 'initialize',
params: {
client: {
name: 'Mock MCP Client',
version: '1.0.0',
},
},
});
console.log('\n--- Server initialized ---\n');
// 2. Send initialized notification
await sendRequest({
jsonrpc: '2.0',
method: 'initialized',
params: {},
});
console.log('\n--- Sent initialized notification ---\n');
// 3. Get tool list
const toolListResult = await sendRequest({
jsonrpc: '2.0',
id: 'req-2',
method: 'tool.list',
params: {},
});
console.log('\n--- Got tool list ---\n');
// 4. Call hello_world tool
const helloResult = await sendRequest({
jsonrpc: '2.0',
id: 'req-3',
method: 'tool.call',
params: {
id: 'call-1',
name: 'hello_world',
arguments: {
name: 'Developer',
language: 'french',
},
},
});
console.log('\n--- Called hello_world tool ---\n');
// 5. Call current_time tool
const timeResult = await sendRequest({
jsonrpc: '2.0',
id: 'req-4',
method: 'tool.call',
params: {
id: 'call-2',
name: 'current_time',
arguments: {
format: 'iso',
},
},
});
console.log('\n--- Called current_time tool ---\n');
console.log('Conversation complete!');
}
// Run the mock client
runConversation().catch(console.error);
```
Install required dependencies:
```bash
npm install --save-dev axios
```
## Environment Variables for Local Development
Create a `.env.development` file for local development:
```
MCP_SERVER_PORT=3000
LOG_LEVEL=debug
```
Install dotenv for environment variable loading:
```bash
npm install --save-dev dotenv
```
Update the local server script to use environment variables:
```typescript
// Add at the beginning of local-server.ts
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.development' });
```
## Iterative Development Workflow
1. **Start the development server**:
```bash
npm run dev
```
2. **Make changes to your code**:
- Modify MCP server implementation
- Add new tools
- Update validation logic
3. **Test changes in real-time**:
- The server will automatically reload when you make changes
- Use the web UI at http://localhost:3000 to test your MCP server
- Or use the mock client: `ts-node scripts/mock-mcp-client.ts`
4. **Debug issues**:
- Set breakpoints in VS Code
- Check the console logs
- Use the VS Code debugger to step through code
5. **Prepare for deployment**:
- Build the project: `npm run build`
- Test the built version: `node dist/scripts/local-server.js`
- Deploy to AWS when ready: `npm run deploy:hello-world`
## Tips for Effective Local Development
1. **Use TypeScript Watch Mode**:
Keep the TypeScript compiler running in watch mode to automatically compile changes.
2. **Set Up Proper Logging**:
Implement detailed logging to help debug issues. Consider using a library like winston.
3. **Test Different MCP Clients**:
Test with both the mock client and real clients like Claude Desktop.
4. **Simulate Edge Cases**:
Create test cases for error conditions, invalid inputs, and edge cases.
5. **Use Git for Version Control**:
Make small, focused commits to track your changes.
6. **Document Your Tools**:
Maintain good documentation for your tools and server behavior.
7. **Write Unit Tests**:
Create unit tests for your tools and core functionality.
## Conclusion
This local development setup allows you to build and test your Lambda-MCP servers efficiently, without needing to deploy to AWS for every change. The web UI and mock client provide easy ways to interact with your MCP server, while the debugging configuration helps you resolve issues quickly.
By following this guide, you can create a productive development environment for building MCP servers with Lambda-MCP.