johnmcarbajal/haskell-mcp-server
If you are the rightful owner of haskell-mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
A Haskell-based Model Context Protocol (MCP) server implementation using Stack.
Haskell MCP Server
A Model Context Protocol (MCP) server implementation in Haskell using Stack.
Features
- JSON-RPC 2.0 Protocol: Full compliance with MCP specification
- WebSocket Communication: Real-time bidirectional communication
- Tool System: Extensible tool registration and execution
- Resource Management: Resource listing and reading capabilities
- Type Safety: Leverages Haskell's type system for robust protocol handling
Built-in Tools
- echo - Echo back input messages
- current_time - Get the current system time
- calculate - Perform basic arithmetic calculations
Built-in Resources
- config://server.json - Server configuration information
Prerequisites
- Stack (Haskell build tool)
- GHC 9.2+ (will be installed automatically by Stack)
Quick Start
-
Clone and build the project:
git clone <your-repo-url> cd haskell-mcp-server stack build -
Run the server:
stack exec haskell-mcp-server-exeOr specify a custom port:
stack exec haskell-mcp-server-exe -- 8080 -
Test the server: The server will listen on
ws://127.0.0.1:3000(or your specified port) for WebSocket connections.
Development
Project Structure
haskell-mcp-server/
├── app/
│ └── Main.hs # Application entry point
├── src/
│ └── MCP/
│ ├── Types.hs # MCP protocol types
│ ├── Server.hs # Core server implementation
│ └── Tools.hs # Tool definitions and handlers
├── test/
│ └── Spec.hs # Test suite
├── package.yaml # Package configuration
├── stack.yaml # Stack configuration
└── README.md
Adding Custom Tools
To add a new tool, create a Tool definition and handler:
myTool :: Tool
myTool = Tool
{ toolName = "my_tool"
, description = "Description of what the tool does"
, inputSchema = object
[ "type" .= ("object" :: Text)
, "properties" .= object
[ "param" .= object
[ "type" .= ("string" :: Text)
, "description" .= ("Parameter description" :: Text)
]
]
, "required" .= (["param"] :: [Text])
]
}
myToolHandler :: Value -> IO ToolResult
myToolHandler args = do
-- Your tool logic here
return $ ToolResult
[ ContentItem "text" "Tool result" ]
Nothing
Then register it in Main.hs:
addTool server myTool myToolHandler
Adding Custom Resources
Similarly, for resources:
let myResource = Resource
{ resourceUri = "custom://my-resource"
, resourceName = "My Resource"
, resourceDescription = Just "Custom resource description"
, mimeType = Just "application/json"
}
addResource server myResource $ return $ object
[ "data" .= ("resource content" :: Text) ]
Testing
Run the test suite:
stack test
Building for Production
Create an optimized build:
stack build --ghc-options="-O2"
Protocol Compliance
This server implements the Model Context Protocol specification:
- Initialization: Proper handshake with protocol version negotiation
- Tool Listing: Dynamic tool discovery via
tools/list - Tool Execution: Safe tool execution via
tools/call - Resource Management: Resource listing and reading via
resources/listandresources/read - Error Handling: Standard JSON-RPC error responses
Dependencies
Key dependencies include:
aeson- JSON serialization/deserializationwebsockets- WebSocket server implementationstm- Software Transactional Memory for concurrent statetext- Efficient text processingcontainers- Data structures (Map, etc.)
License
BSD3 License - see LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Ensure all tests pass (
stack test) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Example Client Usage
Here's how to connect to the server using a WebSocket client:
JavaScript Client Example
const ws = new WebSocket('ws://127.0.0.1:3000');
// Initialize the connection
ws.onopen = () => {
// Send initialize request
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: { major: 2024, minor: 11 },
capabilities: {
roots: { listChanged: true },
sampling: {}
},
clientInfo: {
name: "test-client",
version: "1.0.0"
}
},
id: 1
}));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
console.log('Received:', response);
// After initialization, send initialized notification
if (response.id === 1) {
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "initialized",
params: {}
}));
// List available tools
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "tools/list",
params: {},
id: 2
}));
// Call echo tool
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "echo",
arguments: { message: "Hello, MCP!" }
},
id: 3
}));
}
};
Python Client Example
import asyncio
import websockets
import json
async def test_client():
uri = "ws://127.0.0.1:3000"
async with websockets.connect(uri) as websocket:
# Initialize
init_request = {
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": {"major": 2024, "minor": 11},
"capabilities": {
"roots": {"listChanged": True},
"sampling": {}
},
"clientInfo": {
"name": "python-test-client",
"version": "1.0.0"
}
},
"id": 1
}
await websocket.send(json.dumps(init_request))
response = await websocket.recv()
print("Init response:", json.loads(response))
# Send initialized notification
await websocket.send(json.dumps({
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
}))
# List tools
await websocket.send(json.dumps({
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 2
}))
tools_response = await websocket.recv()
print("Tools:", json.loads(tools_response))
# Call calculator tool
await websocket.send(json.dumps({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "calculate",
"arguments": {"expression": "2 + 3 * 4"}
},
"id": 3
}))
calc_response = await websocket.recv()
print("Calculation result:", json.loads(calc_response))
# Run the client
asyncio.run(test_client())
Configuration
The server can be configured by modifying the defaultServer function in MCP/Server.hs:
-- Custom server configuration
customServer :: IO MCPServer
customServer = do
server <- defaultServer
return server
{ serverInfo = ServerInfo
{ name = "my-custom-server"
, version = "1.0.0"
, protocolVersion = MCPVersion 2024 11
}
}
Troubleshooting
Common Issues
-
Port already in use: Change the port number when starting the server
stack exec haskell-mcp-server-exe -- 8080 -
Build errors: Ensure you have the latest Stack version
stack upgrade stack clean stack build -
WebSocket connection refused: Check that the server is running and the port is correct
Debug Mode
For verbose logging, you can modify the server to include more debug output:
-- In MCP/Server.hs, add more putStrLn statements
handleMessage server req = do
putStrLn $ "Received method: " ++ T.unpack (method req)
-- ... rest of the function
Performance Notes
- The server uses STM (Software Transactional Memory) for thread-safe state management
- WebSocket connections are handled concurrently using lightweight Haskell threads
- Tool handlers should be designed to be non-blocking for optimal performance
API Reference
Core Methods
| Method | Description | Parameters |
|---|---|---|
initialize | Initialize the MCP connection | InitializeParams |
initialized | Notification that initialization is complete | None |
tools/list | List available tools | None |
tools/call | Execute a tool | ToolCallArgs |
resources/list | List available resources | None |
resources/read | Read a resource | {uri: string} |
Error Codes
| Code | Description |
|---|---|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
Roadmap
- Add support for streaming tool responses
- Implement resource subscriptions
- Add built-in file system tools
- Implement tool result caching
- Add configuration file support
- Performance optimizations
- Docker containerization
Support
For questions, issues, or contributions:
- Open an issue on GitHub
- Check the MCP specification for protocol details
- Review the Haskell documentation for language-specific questions