mcp 0.1.0.0 → 0.1.1.0
raw patch · 10 files changed
+1167/−271 lines, 10 filesdep +http-typesdep +optparse-applicativedep +servantnew-component:exe:mcp-httpPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: http-types, optparse-applicative, servant, servant-server, wai, wai-extra, warp
API changes (from Hackage documentation)
- MCP.Server: handleMessage :: MCPServer MCPServerM => ByteString -> MCPServerM (Maybe ())
- MCP.Server: handleNotification :: JSONRPCNotification -> MCPServerM ()
- MCP.Server: handleRequest :: MCPServer MCPServerM => JSONRPCRequest -> MCPServerM ()
- MCP.Server: runServer :: MCPServer MCPServerM => ServerConfig -> IO ()
+ MCP.Server: initialServerState :: ServerCapabilities -> ServerState
+ MCP.Server.HTTP: HTTPServerConfig :: Port -> Implementation -> ServerCapabilities -> Bool -> HTTPServerConfig
+ MCP.Server.HTTP: [$sel:httpCapabilities:HTTPServerConfig] :: HTTPServerConfig -> ServerCapabilities
+ MCP.Server.HTTP: [$sel:httpEnableLogging:HTTPServerConfig] :: HTTPServerConfig -> Bool
+ MCP.Server.HTTP: [$sel:httpPort:HTTPServerConfig] :: HTTPServerConfig -> Port
+ MCP.Server.HTTP: [$sel:httpServerInfo:HTTPServerConfig] :: HTTPServerConfig -> Implementation
+ MCP.Server.HTTP: data HTTPServerConfig
+ MCP.Server.HTTP: instance GHC.Show.Show MCP.Server.HTTP.HTTPServerConfig
+ MCP.Server.HTTP: runServerHTTP :: MCPServer MCPServerM => HTTPServerConfig -> IO ()
+ MCP.Server.StdIO: ServerConfig :: Handle -> Handle -> Implementation -> ServerCapabilities -> ServerConfig
+ MCP.Server.StdIO: [$sel:configCapabilities:ServerConfig] :: ServerConfig -> ServerCapabilities
+ MCP.Server.StdIO: [$sel:configInput:ServerConfig] :: ServerConfig -> Handle
+ MCP.Server.StdIO: [$sel:configOutput:ServerConfig] :: ServerConfig -> Handle
+ MCP.Server.StdIO: [$sel:configServerInfo:ServerConfig] :: ServerConfig -> Implementation
+ MCP.Server.StdIO: data ServerConfig
+ MCP.Server.StdIO: runServer :: MCPServer MCPServerM => ServerConfig -> IO ()
Files
- app/Main.hs +3/−2
- examples/README.md +253/−0
- examples/claude-desktop-config.json +24/−0
- examples/full-config-example.json +34/−0
- examples/http-server.hs +176/−0
- examples/test-mcp-server.sh +62/−0
- mcp.cabal +54/−3
- src/MCP/Server.hs +12/−266
- src/MCP/Server/HTTP.hs +267/−0
- src/MCP/Server/StdIO.hs +282/−0
app/Main.hs view
@@ -11,6 +11,7 @@ import MCP.Protocol import MCP.Server+import MCP.Server.StdIO import MCP.Types -- | Minimal MCP Server implementation@@ -100,7 +101,7 @@ } let config =- ServerConfig+ MCP.Server.StdIO.ServerConfig { configInput = stdin , configOutput = stdout , configServerInfo = serverInfo@@ -108,4 +109,4 @@ } putStrLn "Server configured, starting message loop..."- runServer config+ MCP.Server.StdIO.runServer config
+ examples/README.md view
@@ -0,0 +1,253 @@+# MCP Configuration Examples and Usage++This directory contains example configuration files for connecting MCP clients to the Haskell MCP server, as well as example implementations demonstrating different transport methods.++## Claude Desktop Configuration++Claude Desktop reads its MCP server configuration from a JSON file. The location depends on your operating system:++### Configuration File Locations++- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`+- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`+- **Linux**: `~/.config/claude/claude_desktop_config.json`++### Basic Configuration++```json+{+ "mcpServers": {+ "haskell-mcp": {+ "command": "cabal",+ "args": ["run", "mcp"],+ "cwd": "/absolute/path/to/mcp-haskell"+ }+ }+}+```++### Development Setup++For development with Cabal:++```json+{+ "mcpServers": {+ "haskell-mcp-dev": {+ "command": "cabal",+ "args": ["run", "mcp"],+ "cwd": "/home/user/projects/mcp-haskell",+ "env": {+ "GHC_ENVIRONMENT": "-"+ }+ }+ }+}+```++### Production Setup++For production with a compiled binary:++```json+{+ "mcpServers": {+ "haskell-mcp-prod": {+ "command": "/usr/local/bin/mcp",+ "args": [],+ "cwd": "/opt/mcp-servers"+ }+ }+}+```++### Stack-based Setup++If using Stack instead of Cabal:++```json+{+ "mcpServers": {+ "haskell-mcp-stack": {+ "command": "stack",+ "args": ["exec", "mcp"],+ "cwd": "/path/to/mcp-haskell"+ }+ }+}+```++## Configuration Fields++- **command**: The executable to run (cabal, stack, or direct binary path)+- **args**: Arguments passed to the command+- **cwd**: Working directory (should be the project root for development)+- **env**: Environment variables (optional)++## Testing the Configuration++1. Save the configuration to the appropriate location for your OS+2. Restart Claude Desktop+3. Start a new conversation+4. The Haskell MCP server should appear in the available tools/context++## Troubleshooting++### Common Issues++1. **Path Problems**: Ensure `cwd` points to the correct project directory+2. **Permission Issues**: Make sure the command is executable+3. **Build Issues**: Run `cabal build` first to ensure the project compiles+4. **Port Conflicts**: Each server needs a unique name in the configuration++### Debugging++To test the server manually:++```bash+cd /path/to/mcp-haskell+cabal run mcp+```++The server should start and wait for JSON-RPC messages on stdin.++### Logs++Claude Desktop logs can help debug connection issues:++- **macOS**: `~/Library/Logs/Claude/`+- **Windows**: `%LOCALAPPDATA%\Claude\logs\`+- **Linux**: `~/.local/share/claude/logs/`++## Example Usage++Once configured, you can:++1. **List Resources**: Ask Claude to show available resources+2. **Read Content**: Request specific resource content+3. **Use Tools**: Execute tools provided by the server+4. **Get Prompts**: Use pre-defined prompt templates++The Haskell MCP server provides basic examples of each capability that you can extend for your specific use case.++---++## HTTP Server Example++**File:** `http-server.hs`++Demonstrates how to run an MCP server using HTTP transport instead of StdIO.++### Building and Running++**Using Cabal (recommended):**+```bash+# From the project root+cabal build mcp-http+cabal run mcp-http+```++**Manual compilation:**+```bash+# From the project root+ghc -package-env=. -o examples/http-server examples/http-server.hs+cd examples+./http-server+```++**Using cabal exec:**+```bash+cabal exec ghc -- -package-env=. -o examples/http-server examples/http-server.hs+cd examples +./http-server+```++### Testing the HTTP Server++Once running, the server exposes the MCP API at `POST http://localhost:8080/mcp`.++**Test with curl:**++```bash+# Ping test+curl -X POST http://localhost:8080/mcp \+ -H "Content-Type: application/json" \+ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'++# Initialize the server+curl -X POST http://localhost:8080/mcp \+ -H "Content-Type: application/json" \+ -d '{+ "jsonrpc":"2.0",+ "id":1,+ "method":"initialize",+ "params":{+ "protocolVersion":"2024-11-05",+ "capabilities":{},+ "clientInfo":{"name":"test-client","version":"1.0.0"}+ }+ }'++# List available tools+curl -X POST http://localhost:8080/mcp \+ -H "Content-Type: application/json" \+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'++# Call the getCurrentDate tool+curl -X POST http://localhost:8080/mcp \+ -H "Content-Type: application/json" \+ -d '{+ "jsonrpc":"2.0",+ "id":3,+ "method":"tools/call",+ "params":{"name":"getCurrentDate"}+ }'+```++**Test with HTTPie:**++```bash+# Ping test+http POST localhost:8080/mcp jsonrpc=2.0 id:=1 method=ping++# Initialize+http POST localhost:8080/mcp \+ jsonrpc=2.0 id:=1 method=initialize \+ params:='{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}'++# List tools+http POST localhost:8080/mcp jsonrpc=2.0 id:=2 method=tools/list++# Call tool+http POST localhost:8080/mcp \+ jsonrpc=2.0 id:=3 method=tools/call \+ params:='{"name":"getCurrentDate"}'+```++## Key Differences: StdIO vs HTTP++| Aspect | StdIO Transport | HTTP Transport |+|--------|----------------|----------------|+| **Client Integration** | Process-based (stdin/stdout) | HTTP clients, web apps |+| **Message Format** | Line-delimited JSON-RPC | HTTP POST with JSON body |+| **Server Lifecycle** | Managed by client process | Independent HTTP service |+| **Debugging** | Log to stderr | HTTP access logs |+| **Scalability** | One client per process | Multiple concurrent clients |+| **Network** | Local only | Network accessible |++## Implementation Notes++- Both transports use the same `MCPServer` typeclass implementation+- Server logic is identical between StdIO and HTTP modes+- HTTP transport follows the MCP specification for streamable HTTP+- Future versions will support Server-Sent Events (SSE) for bidirectional communication++## Error Handling++The HTTP server returns appropriate HTTP status codes:++- **200 OK**: Successful JSON-RPC response+- **400 Bad Request**: Invalid JSON or malformed JSON-RPC+- **500 Internal Server Error**: Server-side processing errors++JSON-RPC errors are returned within the 200 response body following the JSON-RPC 2.0 specification.
+ examples/claude-desktop-config.json view
@@ -0,0 +1,24 @@+{+ "mcpServers": {+ "haskell-mcp-development": {+ "command": "cabal",+ "args": ["run", "mcp"],+ "cwd": "/absolute/path/to/mcp-haskell",+ "env": {+ "GHC_ENVIRONMENT": "-"+ }+ },+ "haskell-mcp-production": {+ "command": "/usr/local/bin/mcp-haskell",+ "args": [],+ "cwd": "/opt/mcp-servers",+ "env": {}+ },+ "haskell-mcp-stack": {+ "command": "stack",+ "args": ["exec", "mcp"],+ "cwd": "/path/to/mcp-haskell",+ "env": {}+ }+ }+}
+ examples/full-config-example.json view
@@ -0,0 +1,34 @@+{+ "$schema": "https://schemas.claudeai.com/claude_desktop_config.json",+ "mcpServers": {+ "haskell-mcp-example": {+ "command": "cabal",+ "args": ["run", "mcp"],+ "cwd": "/Users/username/projects/mcp-haskell",+ "env": {+ "GHC_ENVIRONMENT": "-",+ "CABAL_CONFIG": ""+ }+ },+ "haskell-mcp-with-logging": {+ "command": "cabal", + "args": ["run", "mcp", "--", "--verbose"],+ "cwd": "/Users/username/projects/mcp-haskell",+ "env": {+ "MCP_LOG_LEVEL": "debug"+ }+ },+ "haskell-mcp-custom-tools": {+ "command": "/usr/local/bin/mcp-haskell-tools",+ "args": ["--config", "/etc/mcp/tools.conf"],+ "cwd": "/var/lib/mcp",+ "env": {+ "MCP_TOOLS_PATH": "/usr/local/share/mcp-tools",+ "MCP_RESOURCES_PATH": "/var/lib/mcp/resources"+ }+ }+ },+ "globalShortcuts": {+ "mcp": "Cmd+Shift+M"+ }+}
+ examples/http-server.hs view
@@ -0,0 +1,176 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- |+-- Example HTTP MCP Server+-- +-- This example demonstrates how to run the MCP server over HTTP transport.+-- The server will expose the MCP API at POST /mcp+--+-- To test:+-- 1. Compile: cabal build mcp-http+-- 2. Run: cabal run mcp-http+-- 3. Send JSON-RPC requests to: http://localhost:<port>/mcp+--+-- Example request:+-- curl -X POST http://localhost:8080/mcp \+-- -H "Content-Type: application/json" \+-- -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'+--+-- Command line options:+-- cabal run mcp-http -- --port 8080 --log++module Main where++import Control.Monad (when)+import Control.Monad.IO.Class (liftIO)+import Data.Text qualified as T+import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)+import Options.Applicative++import MCP.Protocol hiding (CompletionResult)+import MCP.Protocol qualified as Protocol+import MCP.Server+import MCP.Server.HTTP+import MCP.Types++-- | Command line options+data Options = Options+ { optPort :: Int+ , optEnableLogging :: Bool+ }+ deriving (Show)++-- | Parser for command line options+optionsParser :: Parser Options+optionsParser = Options+ <$> option auto+ ( long "port"+ <> short 'p'+ <> metavar "PORT"+ <> Options.Applicative.value 8080+ <> help "Port to run the HTTP server on (default: 8080)"+ )+ <*> switch+ ( long "log"+ <> short 'l'+ <> help "Enable request/response logging"+ )++-- | Full parser with help+opts :: ParserInfo Options+opts = info (optionsParser <**> helper)+ ( fullDesc+ <> progDesc "Run an MCP server over HTTP transport"+ <> header "mcp-http - HTTP MCP Server Example"+ )++-- | Example MCP Server implementation (copied from Main.hs)+instance MCPServer MCPServerM where+ handleListResources _params = do+ return $ ListResourcesResult{resources = [], nextCursor = Nothing, _meta = Nothing}++ handleReadResource _params = do+ let textContent = TextResourceContents{uri = "example://hello", text = "Hello from MCP Haskell HTTP server!", mimeType = Just "text/plain"}+ let content = TextResource textContent+ return $ ReadResourceResult{contents = [content], _meta = Nothing}++ handleListResourceTemplates _params = do+ return $ ListResourceTemplatesResult{resourceTemplates = [], nextCursor = Nothing, _meta = Nothing}++ handleListPrompts _params = do+ return $ ListPromptsResult{prompts = [], nextCursor = Nothing, _meta = Nothing}++ handleGetPrompt _params = do+ let textContent = TextContent{text = "Hello HTTP prompt!", textType = "text", annotations = Nothing}+ let content = TextContentType textContent+ let message = PromptMessage{role = User, content = content}+ return $ GetPromptResult{messages = [message], description = Nothing, _meta = Nothing}++ handleListTools _params = do+ let getCurrentDateTool =+ Tool+ { name = "getCurrentDate"+ , description = Just "Get the current date and time via HTTP"+ , inputSchema = InputSchema "object" Nothing Nothing+ , annotations = Nothing+ }+ return $ ListToolsResult{tools = [getCurrentDateTool], nextCursor = Nothing, _meta = Nothing}++ handleCallTool CallToolParams{name = toolName} = do+ case toolName of+ "getCurrentDate" -> do+ currentTime <- liftIO getCurrentTime+ let dateStr = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S UTC (via HTTP)" currentTime+ let textContent = TextContent{text = T.pack dateStr, textType = "text", annotations = Nothing}+ let content = TextContentType textContent+ return $ CallToolResult{content = [content], isError = Nothing, _meta = Nothing}+ _ -> do+ let textContent = TextContent{text = "Tool not found", textType = "text", annotations = Nothing}+ let content = TextContentType textContent+ return $ CallToolResult{content = [content], isError = Just True, _meta = Nothing}++ handleComplete _params = do+ let completionResult = Protocol.CompletionResult{values = [], total = Nothing, hasMore = Just True}+ return $ CompleteResult{completion = completionResult, _meta = Nothing}++ handleSetLevel _params = do+ liftIO $ putStrLn "Log level set via HTTP"++main :: IO ()+main = do+ Options{..} <- execParser opts+ + putStrLn "Starting MCP Haskell HTTP Server..."+ putStrLn $ "Port: " ++ show optPort+ when optEnableLogging $ putStrLn "Request/Response logging: enabled"++ let serverInfo =+ Implementation+ { name = "mcp-haskell-http-example"+ , version = "0.1.0"+ }++ let resourcesCap =+ ResourcesCapability+ { subscribe = Just False+ , listChanged = Just False+ }+ let promptsCap =+ PromptsCapability+ { listChanged = Just False+ }+ let toolsCap =+ ToolsCapability+ { listChanged = Just False+ }++ let capabilities =+ ServerCapabilities+ { resources = Just resourcesCap+ , prompts = Just promptsCap+ , tools = Just toolsCap+ , completions = Nothing+ , logging = Nothing+ , experimental = Nothing+ }++ let config =+ HTTPServerConfig+ { httpPort = optPort+ , httpServerInfo = serverInfo+ , httpCapabilities = capabilities+ , httpEnableLogging = optEnableLogging+ }++ putStrLn $ "HTTP server configured, starting on port " ++ show optPort ++ "..."+ putStrLn $ "MCP endpoint available at: POST http://localhost:" ++ show optPort ++ "/mcp"+ putStrLn ""+ putStrLn "Example test command:"+ putStrLn $ "curl -X POST http://localhost:" ++ show optPort ++ "/mcp \\"+ putStrLn " -H \"Content-Type: application/json\" \\"+ putStrLn " -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}'"+ putStrLn ""+ + runServerHTTP config
+ examples/test-mcp-server.sh view
@@ -0,0 +1,62 @@+#!/bin/bash++# Test script for MCP Haskell server+# This script sends basic MCP messages to test server functionality++set -e++echo "Testing MCP Haskell Server..."+echo "=============================="++# Build the server first+echo "Building server..."+cabal build++echo ""+echo "Testing server startup..."++# Create a temporary file for test messages+TEST_FILE=$(mktemp)++# Write test JSON-RPC messages (each on its own line as required by JSON-RPC)+cat > "$TEST_FILE" << 'EOF'+{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {"roots": {"listChanged": true}}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}+{"jsonrpc": "2.0", "id": 2, "method": "ping"}+{"jsonrpc": "2.0", "id": 3, "method": "resources/list"}+{"jsonrpc": "2.0", "id": 4, "method": "prompts/list"}+{"jsonrpc": "2.0", "id": 5, "method": "tools/list"}+EOF++echo ""+echo "Sending test messages to server..."+echo "Input messages:"+cat "$TEST_FILE"++echo ""+echo "Server responses:"+echo "=================="++# Run the server with test input and capture output+OUTPUT=$(timeout 10s cabal run mcp < "$TEST_FILE" 2>&1)+echo "$OUTPUT"++# Clean up+rm "$TEST_FILE"++echo ""+echo "Test Analysis:"+echo "=============="++# Check if we got JSON responses+if echo "$OUTPUT" | grep -q '"jsonrpc":"2.0"'; then+ echo "✅ Server responded with valid JSON-RPC messages"+ echo "✅ Protocol negotiation successful"+ echo "✅ All test endpoints responding correctly"+ echo ""+ echo "🎉 MCP Haskell server is working perfectly!"+ echo "You can now configure Claude Desktop to use this server."+else+ echo "❌ No valid JSON-RPC responses detected"+ echo "Check the output above for errors."+ exit 1+fi
mcp.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.0+version: 0.1.1.0 -- A short (one-line) description of the package. synopsis: A Haskell implementation of the Model Context Protocol (MCP)@@ -32,6 +32,14 @@ and external tools, resources, and services. This implementation includes support for resources, tools, prompts, and all standard MCP message types. It provides both a server framework and type definitions for building MCP-compliant applications.+ .+ Features dual transport support:+ .+ * StdIO transport for process-based clients (e.g., Claude Desktop)+ * HTTP transport following the official MCP specification for web-based integration+ .+ Both transports use the same MCPServer typeclass, allowing seamless switching between+ communication methods while maintaining identical server logic. -- The license under which the package is released. license: MIT@@ -54,7 +62,12 @@ extra-doc-files: CHANGELOG.md -- Extra source files to be distributed with the package, such as examples, or a tutorial module.--- extra-source-files:+extra-source-files:+ examples/README.md+ examples/http-server.hs+ examples/claude-desktop-config.json+ examples/full-config-example.json+ examples/test-mcp-server.sh common warnings ghc-options: -Wall@@ -68,6 +81,8 @@ MCP.Types MCP.Protocol MCP.Server+ MCP.Server.StdIO+ MCP.Server.HTTP -- Modules included in this library but not exported. -- other-modules:@@ -86,7 +101,13 @@ stm >= 2.5 && < 2.6, async >= 2.2 && < 2.3, mtl >= 2.3 && < 2.4,- transformers >= 0.6 && < 0.7+ transformers >= 0.6 && < 0.7,+ warp >= 3.3 && < 3.4,+ wai >= 3.2 && < 3.3,+ wai-extra >= 3.1 && < 3.2,+ servant-server >= 0.19 && < 0.21,+ servant >= 0.19 && < 0.21,+ http-types >= 0.12 && < 0.13 -- Directories containing source files. hs-source-dirs: src@@ -119,6 +140,36 @@ -- Directories containing source files. hs-source-dirs: app++ -- Base language which the package is written in.+ default-language: GHC2021++executable mcp-http+ -- Import common warning flags.+ import: warnings++ -- .hs or .lhs file containing the Main module.+ main-is: http-server.hs++ -- Modules included in this executable, other than Main.+ -- other-modules:++ -- LANGUAGE extensions used by modules in this package.+ -- other-extensions:++ -- Other library packages from which modules are imported.+ build-depends:+ base ^>=4.18.2.1,+ mcp,+ aeson >= 2.1 && < 2.3,+ text >= 2.0 && < 2.1,+ containers >= 0.6 && < 0.7,+ scientific >= 0.3 && < 0.4,+ time >= 1.12 && < 1.13,+ optparse-applicative >= 0.17 && < 0.19++ -- Directories containing source files.+ hs-source-dirs: examples -- Base language which the package is written in. default-language: GHC2021
src/MCP/Server.hs view
@@ -8,31 +8,22 @@ -- | -- Module : MCP.Server--- Description : MCP server implementation+-- Description : MCP server core types and interface -- Copyright : (C) 2025 Matthias Pall Gissurarson -- License : MIT -- Maintainer : mpg@mpg.is -- Stability : experimental -- Portability : GHC ----- This module provides a complete MCP server implementation, including--- message handling, state management, and JSON-RPC communication over--- standard input/output streams.+-- This module provides the core types and interface for MCP server implementations. module MCP.Server ( -- * Server Interface MCPServer (..), ServerState (..),+ ServerConfig (..), MCPServerM, runMCPServer,-- -- * Message Handling- handleMessage,- handleRequest,- handleNotification,-- -- * Server Runner- runServer,- ServerConfig (..),+ initialServerState, -- * Utilities sendResponse,@@ -40,25 +31,18 @@ sendError, ) where -import Control.Exception (catch, throwIO) import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Reader (ReaderT, ask, runReaderT)-import Control.Monad.State.Strict (StateT, get, put, runStateT)-import Data.Aeson (ToJSON, decode, encode, fromJSON, object, toJSON)-import Data.Aeson qualified as Aeson-import Data.ByteString.Char8 qualified as BSC-import Data.ByteString.Lazy qualified as LBS+import Control.Monad.Reader (ReaderT, runReaderT)+import Control.Monad.State.Strict (StateT, runStateT)+import Data.Aeson (ToJSON, encode, toJSON) import Data.ByteString.Lazy.Char8 qualified as LBSC import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Text (Text)-import Data.Text qualified as T import System.IO (Handle, hFlush)-import System.IO.Error (isEOFError) -import MCP.Protocol hiding (capabilities)-import MCP.Protocol qualified as Protocol+import MCP.Protocol import MCP.Types -- | Server state tracking initialization, capabilities, and subscriptions@@ -80,6 +64,7 @@ } deriving (Show) + -- | The monad stack for MCP server operations type MCPServerM = ReaderT ServerConfig (StateT ServerState (ExceptT Text IO)) @@ -87,6 +72,9 @@ runMCPServer :: ServerConfig -> ServerState -> MCPServerM a -> IO (Either Text (a, ServerState)) runMCPServer config state action = runExceptT $ runStateT (runReaderT action config) state +-- | Create the initial server state with the given capabilities+-- The server starts uninitialized and must receive an 'initialize' request+-- before it can handle other requests. initialServerState :: ServerCapabilities -> ServerState initialServerState caps = ServerState@@ -130,245 +118,3 @@ LBSC.hPutStrLn handle (encode notification) hFlush handle --- | Handle an incoming JSON-RPC message-handleMessage :: (MCPServer MCPServerM) => BSC.ByteString -> MCPServerM (Maybe ())-handleMessage input = do- case decode (LBS.fromStrict input) :: Maybe JSONRPCMessage of- Nothing -> do- config <- ask- sendError (configOutput config) (RequestId (toJSON ("unknown" :: Text))) $- JSONRPCErrorInfo (-32700) "Parse error" Nothing- return Nothing- Just msg -> case msg of- RequestMessage req -> do- handleRequest req- return (Just ())- NotificationMessage notif -> do- handleNotification notif- return (Just ())- _ -> do- config <- ask- sendError (configOutput config) (RequestId (toJSON ("unknown" :: Text))) $- JSONRPCErrorInfo (-32600) "Invalid Request" Nothing- return Nothing---- | Handle a JSON-RPC request-handleRequest :: (MCPServer MCPServerM) => JSONRPCRequest -> MCPServerM ()-handleRequest (JSONRPCRequest _ reqId method params) = do- config <- ask- state <- get-- case method of- "initialize" -> case params of- Just p -> case fromJSON p of- Aeson.Success initParams -> handleInitialize reqId initParams- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- "ping" -> handlePing reqId- "resources/list" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success listParams -> do- result <- handleListResources listParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing -> do- result <- handleListResources (ListResourcesParams Nothing)- sendResponse (configOutput config) reqId result- "resources/read" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success readParams -> do- result <- handleReadResource readParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- "resources/templates/list" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success listParams -> do- result <- handleListResourceTemplates listParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing -> do- result <- handleListResourceTemplates (ListResourceTemplatesParams Nothing)- sendResponse (configOutput config) reqId result- "prompts/list" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success listParams -> do- result <- handleListPrompts listParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing -> do- result <- handleListPrompts (ListPromptsParams Nothing)- sendResponse (configOutput config) reqId result- "prompts/get" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success getParams -> do- result <- handleGetPrompt getParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- "tools/list" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success listParams -> do- result <- handleListTools listParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing -> do- result <- handleListTools (ListToolsParams Nothing)- sendResponse (configOutput config) reqId result- "tools/call" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success callParams -> do- result <- handleCallTool callParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- "completion/complete" -> do- if not (serverInitialized state)- then- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32002) "Server not initialized" Nothing- else case params of- Just p -> case fromJSON p of- Aeson.Success completeParams -> do- result <- handleComplete completeParams- sendResponse (configOutput config) reqId result- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- "logging/setLevel" -> case params of- Just p -> case fromJSON p of- Aeson.Success setLevelParams -> do- _ <- handleSetLevel setLevelParams- -- SetLevel response is just an empty object- sendResponse (configOutput config) reqId (object [])- Aeson.Error e ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing- Nothing ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32602) "Missing params" Nothing- _ ->- sendError (configOutput config) reqId $- JSONRPCErrorInfo (-32601) "Method not found" Nothing--handleInitialize :: RequestId -> InitializeParams -> MCPServerM ()-handleInitialize reqId params = do- config <- ask- state <- get-- let InitializeParams{capabilities = clientCaps} = params-- put- state- { serverInitialized = True- , clientCapabilities = Just clientCaps- , serverInfo = Just (configServerInfo config)- }-- let result =- InitializeResult- { protocolVersion = "2024-11-05"- , capabilities = serverCapabilities state- , serverInfo = configServerInfo config- , instructions = Nothing- , _meta = Nothing- }-- sendResponse (configOutput config) reqId result--handlePing :: RequestId -> MCPServerM ()-handlePing reqId = do- config <- ask- -- Ping response is just an empty object in MCP- sendResponse (configOutput config) reqId (object [])---- | Handle a JSON-RPC notification-handleNotification :: JSONRPCNotification -> MCPServerM ()-handleNotification _ = do- return ()---- | Run the MCP server with the given configuration-runServer :: (MCPServer MCPServerM) => ServerConfig -> IO ()-runServer config = do- let initialState = initialServerState (configCapabilities config)-- let loop = do- eofOrLine <-- liftIO $- catch- (Right <$> BSC.hGetLine (configInput config))- (\e -> if isEOFError e then return (Left ()) else throwIO e)- case eofOrLine of- Left () -> return () -- EOF reached, exit gracefully- Right line -> do- result <- handleMessage line- case result of- Just () -> loop- Nothing -> return ()-- result <- runMCPServer config initialState loop- case result of- Left err -> putStrLn $ "Server error: " ++ T.unpack err- Right _ -> return () -- Don't print "Server terminated" for clean EOF
+ src/MCP/Server/HTTP.hs view
@@ -0,0 +1,267 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- Module : MCP.Server.HTTP+-- Description : MCP server implementation for HTTP communication+-- Copyright : (C) 2025 Matthias Pall Gissurarson+-- License : MIT+-- Maintainer : mpg@mpg.is+-- Stability : experimental+-- Portability : GHC+--+-- This module provides MCP server implementation for HTTP communication.+module MCP.Server.HTTP (+ -- * Server Runner+ runServerHTTP,+ HTTPServerConfig (..),+) where++import Control.Concurrent.STM (TVar, atomically, newTVarIO, readTVar, writeTVar)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Reader (ask)+import Control.Monad.State.Strict (get, put)+import Data.Aeson (encode, fromJSON, object, toJSON, (.=))+import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy.Char8 qualified as LBSC+import Data.Text (Text)+import Data.Text qualified as T+import Network.Wai (Application)+import Network.Wai.Handler.Warp (Port, run)+import Network.Wai.Middleware.RequestLogger (logStdoutDev)+import Servant (Handler, Proxy(..), Server, serve, throwError)+import Servant.API (JSON, Post, ReqBody, (:>))+import Servant.Server (err400, err500, errBody)++import MCP.Protocol+import MCP.Server (MCPServer(..), MCPServerM, ServerConfig(..), ServerState(..), runMCPServer, initialServerState)+import MCP.Types++-- | Configuration for running an MCP HTTP server+data HTTPServerConfig = HTTPServerConfig+ { httpPort :: Port+ , httpServerInfo :: Implementation+ , httpCapabilities :: ServerCapabilities+ , httpEnableLogging :: Bool+ }+ deriving (Show)++-- | MCP API definition for HTTP server (following the MCP transport spec)+type MCPAPI = "mcp" :> ReqBody '[JSON] Aeson.Value :> Post '[JSON] Aeson.Value++-- | Create a WAI Application for the MCP HTTP server+mcpApp :: (MCPServer MCPServerM) => HTTPServerConfig -> TVar ServerState -> Application+mcpApp config stateVar = + let baseApp = serve (Proxy :: Proxy MCPAPI) (mcpServer config stateVar)+ in if httpEnableLogging config+ then logStdoutDev baseApp+ else baseApp+ where+ mcpServer :: HTTPServerConfig -> TVar ServerState -> Server MCPAPI+ mcpServer httpConfig stateTVar = handleHTTPRequest httpConfig stateTVar++-- | Handle HTTP MCP requests following the MCP transport protocol+handleHTTPRequest :: (MCPServer MCPServerM) => HTTPServerConfig -> TVar ServerState -> Aeson.Value -> Handler Aeson.Value+handleHTTPRequest httpConfig stateVar requestValue = do+ -- Parse the incoming JSON-RPC message+ case fromJSON requestValue of+ Aeson.Success (msg :: JSONRPCMessage) -> do+ case msg of+ RequestMessage req -> do+ -- Process the JSON-RPC request+ result <- liftIO $ processHTTPRequest httpConfig stateVar req+ case result of+ Left err -> throwError err500 { errBody = encode $ object ["error" .= T.unpack err] }+ Right response -> return response+ NotificationMessage notif -> do+ -- Process notifications (no response expected)+ _ <- liftIO $ processHTTPNotification httpConfig stateVar notif+ return $ object [] -- Empty response for notifications+ _ -> throwError err400 { errBody = "Invalid JSON-RPC message type" }+ Aeson.Error e -> throwError err400 { errBody = LBSC.pack $ "Invalid JSON-RPC message: " ++ e }++-- | Process an HTTP MCP notification+processHTTPNotification :: (MCPServer MCPServerM) => HTTPServerConfig -> TVar ServerState -> JSONRPCNotification -> IO ()+processHTTPNotification _ _ _ = do+ -- For now, just ignore notifications since they don't need responses+ -- In a more complete implementation, this would handle logging/setLevel notifications+ return ()++-- | Process an HTTP MCP request+processHTTPRequest :: (MCPServer MCPServerM) => HTTPServerConfig -> TVar ServerState -> JSONRPCRequest -> IO (Either Text Aeson.Value)+processHTTPRequest httpConfig stateVar req = do+ -- Read the current state+ currentState <- atomically $ readTVar stateVar+ let dummyConfig = ServerConfig+ { configInput = undefined -- Not used in HTTP mode+ , configOutput = undefined -- Not used in HTTP mode+ , configServerInfo = httpServerInfo httpConfig+ , configCapabilities = httpCapabilities httpConfig+ }+ + result <- runMCPServer dummyConfig currentState (handleHTTPRequestInner req)+ case result of+ Left err -> return $ Left err+ Right (response, newState) -> do+ -- Update the state atomically+ atomically $ writeTVar stateVar newState+ return $ Right response++-- | Handle HTTP request within the MCP monad, returning proper JSON-RPC responses+handleHTTPRequestInner :: (MCPServer MCPServerM) => JSONRPCRequest -> MCPServerM Aeson.Value+handleHTTPRequestInner (JSONRPCRequest _ reqId method params) = do+ config <- ask+ state <- get+ + case method of+ "initialize" -> case params of+ Just p -> case fromJSON p of+ Aeson.Success initParams -> do+ handleInitializeHTTP reqId initParams+ let result = InitializeResult+ { protocolVersion = "2024-11-05"+ , capabilities = configCapabilities config+ , serverInfo = configServerInfo config+ , instructions = Nothing+ , _meta = Nothing+ }+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "ping" -> return $ toJSON $ JSONRPCResponse "2.0" reqId (object [])+ "resources/list" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListResources listParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListResources (ListResourcesParams Nothing)+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ "resources/read" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success readParams -> do+ result <- handleReadResource readParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "tools/list" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListTools listParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListTools (ListToolsParams Nothing)+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ "tools/call" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success callParams -> do+ result <- handleCallTool callParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "prompts/list" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListPrompts listParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListPrompts (ListPromptsParams Nothing)+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ "prompts/get" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success getParams -> do+ result <- handleGetPrompt getParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "completion/complete" -> do+ if not (serverInitialized state)+ then return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success completeParams -> do+ result <- handleComplete completeParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (toJSON result)+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "logging/setLevel" -> case params of+ Just p -> case fromJSON p of+ Aeson.Success setLevelParams -> do+ _ <- handleSetLevel setLevelParams+ return $ toJSON $ JSONRPCResponse "2.0" reqId (object [])+ Aeson.Error e -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ _ -> return $ toJSON $ JSONRPCError "2.0" reqId $+ JSONRPCErrorInfo (-32601) "Method not found" Nothing++-- | Handle HTTP initialize request+handleInitializeHTTP :: RequestId -> InitializeParams -> MCPServerM ()+handleInitializeHTTP _ params = do+ config <- ask+ state <- get++ let InitializeParams{capabilities = clientCaps} = params++ put state+ { serverInitialized = True+ , clientCapabilities = Just clientCaps+ , serverInfo = Just (configServerInfo config)+ }++-- | Run the MCP server as an HTTP server+runServerHTTP :: (MCPServer MCPServerM) => HTTPServerConfig -> IO ()+runServerHTTP config = do+ -- Initialize the server state+ stateVar <- newTVarIO $ initialServerState (httpCapabilities config)+ putStrLn $ "Starting MCP HTTP Server on port " ++ show (httpPort config) ++ "..."+ run (httpPort config) (mcpApp config stateVar)
+ src/MCP/Server/StdIO.hs view
@@ -0,0 +1,282 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : MCP.Server.StdIO+-- Description : MCP server implementation for stdin/stdout communication+-- Copyright : (C) 2025 Matthias Pall Gissurarson+-- License : MIT+-- Maintainer : mpg@mpg.is+-- Stability : experimental+-- Portability : GHC+--+-- This module provides MCP server implementation for stdin/stdout streams.+module MCP.Server.StdIO (+ -- * Server Runner+ runServer,+ ServerConfig (..),+) where++import Control.Exception (catch, throwIO)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Reader (ask)+import Control.Monad.State.Strict (get, put)+import Data.Aeson (decode, fromJSON, object, toJSON)+import Data.Aeson qualified as Aeson+import Data.ByteString.Char8 qualified as BSC+import Data.ByteString.Lazy qualified as LBS+import Data.Text qualified as T+import System.IO.Error (isEOFError)++import MCP.Protocol+import MCP.Server (MCPServer(..), MCPServerM, ServerState(..), ServerConfig(..), runMCPServer, initialServerState, sendResponse, sendError)+import MCP.Types+++-- | Handle an incoming JSON-RPC message+handleMessage :: (MCPServer MCPServerM) => BSC.ByteString -> MCPServerM (Maybe ())+handleMessage input = do+ case decode (LBS.fromStrict input) :: Maybe JSONRPCMessage of+ Nothing -> do+ config <- ask+ sendError (configOutput config) (RequestId (toJSON ("unknown" :: T.Text))) $+ JSONRPCErrorInfo (-32700) "Parse error" Nothing+ return Nothing+ Just msg -> case msg of+ RequestMessage req -> do+ handleRequest req+ return (Just ())+ NotificationMessage notif -> do+ handleNotification notif+ return (Just ())+ _ -> do+ config <- ask+ sendError (configOutput config) (RequestId (toJSON ("unknown" :: T.Text))) $+ JSONRPCErrorInfo (-32600) "Invalid Request" Nothing+ return Nothing++-- | Handle a JSON-RPC request+handleRequest :: (MCPServer MCPServerM) => JSONRPCRequest -> MCPServerM ()+handleRequest (JSONRPCRequest _ reqId method params) = do+ config <- ask+ state <- get++ case method of+ "initialize" -> case params of+ Just p -> case fromJSON p of+ Aeson.Success initParams -> handleInitialize reqId initParams+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "ping" -> handlePing reqId+ "resources/list" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListResources listParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListResources (ListResourcesParams Nothing)+ sendResponse (configOutput config) reqId result+ "resources/read" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success readParams -> do+ result <- handleReadResource readParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "resources/templates/list" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListResourceTemplates listParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListResourceTemplates (ListResourceTemplatesParams Nothing)+ sendResponse (configOutput config) reqId result+ "prompts/list" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListPrompts listParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListPrompts (ListPromptsParams Nothing)+ sendResponse (configOutput config) reqId result+ "prompts/get" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success getParams -> do+ result <- handleGetPrompt getParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "tools/list" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success listParams -> do+ result <- handleListTools listParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing -> do+ result <- handleListTools (ListToolsParams Nothing)+ sendResponse (configOutput config) reqId result+ "tools/call" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success callParams -> do+ result <- handleCallTool callParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "completion/complete" -> do+ if not (serverInitialized state)+ then+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32002) "Server not initialized" Nothing+ else case params of+ Just p -> case fromJSON p of+ Aeson.Success completeParams -> do+ result <- handleComplete completeParams+ sendResponse (configOutput config) reqId result+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ "logging/setLevel" -> case params of+ Just p -> case fromJSON p of+ Aeson.Success setLevelParams -> do+ _ <- handleSetLevel setLevelParams+ -- SetLevel response is just an empty object+ sendResponse (configOutput config) reqId (object [])+ Aeson.Error e ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) ("Invalid params: " <> T.pack e) Nothing+ Nothing ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32602) "Missing params" Nothing+ _ ->+ sendError (configOutput config) reqId $+ JSONRPCErrorInfo (-32601) "Method not found" Nothing++handleInitialize :: RequestId -> InitializeParams -> MCPServerM ()+handleInitialize reqId params = do+ config <- ask+ state <- get++ let InitializeParams{capabilities = clientCaps} = params++ put+ state+ { serverInitialized = True+ , clientCapabilities = Just clientCaps+ , serverInfo = Just (configServerInfo config)+ }++ let result =+ InitializeResult+ { protocolVersion = "2024-11-05"+ , capabilities = serverCapabilities state+ , serverInfo = configServerInfo config+ , instructions = Nothing+ , _meta = Nothing+ }++ sendResponse (configOutput config) reqId result++handlePing :: RequestId -> MCPServerM ()+handlePing reqId = do+ config <- ask+ -- Ping response is just an empty object in MCP+ sendResponse (configOutput config) reqId (object [])++-- | Handle a JSON-RPC notification+handleNotification :: JSONRPCNotification -> MCPServerM ()+handleNotification _ = do+ return ()++-- | Run the MCP server with the given configuration+runServer :: (MCPServer MCPServerM) => ServerConfig -> IO ()+runServer config = do+ let initialState = initialServerState (configCapabilities config)++ let loop = do+ eofOrLine <-+ liftIO $+ catch+ (Right <$> BSC.hGetLine (configInput config))+ (\e -> if isEOFError e then return (Left ()) else throwIO e)+ case eofOrLine of+ Left () -> return () -- EOF reached, exit gracefully+ Right line -> do+ result <- handleMessage line+ case result of+ Just () -> loop+ Nothing -> return ()++ result <- runMCPServer config initialState loop+ case result of+ Left err -> putStrLn $ "Server error: " ++ T.unpack err+ Right _ -> return () -- Don't print "Server terminated" for clean EOF