mcp-0.3.0.1: test/MCP/TestServer.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{- |
Module: MCP.TestServer
License: MPL-2.0
Maintainer: <matti@dpella.io>, <lobo@dpella.io>
This module provides server configuration utilities for testing the MCP server
implementation.
-}
module MCP.TestServer where
import Control.Concurrent.MVar (MVar, newMVar)
import Data.Aeson (FromJSON, ToJSON, fromJSON, object, toJSON, (.=))
import Data.Aeson qualified as Aeson
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import MCP.Server
import Network.Wai (Application)
import Servant (Context (..), Proxy (..), serveWithContext)
import Servant.Auth.Server qualified as AuthServer
-- * Server Configuration Types and Functions
-- ** Test user
-- | A simple test user for MCP tests
data TestUser = TestUser
{ userId :: Text
, userName :: Text
, userEmail :: Text
}
deriving (Eq, Show, Generic)
instance ToJSON TestUser
instance FromJSON TestUser
instance AuthServer.ToJWT TestUser
instance AuthServer.FromJWT TestUser
type instance MCPHandlerState = TestStateHandler
type instance MCPHandlerUser = TestUser
-- ** MCP Handler State
-- | A simple state handler for MCP tests
newtype TestStateHandler = TestStateHandler {handle_curr_user :: Maybe Text}
-- | Initialize a fresh test state handler
initializeTestState :: TestStateHandler
initializeTestState = TestStateHandler{handle_curr_user = Nothing}
-- | Initialization of the handler with the current user
mb_handler_init :: Maybe (TestUser -> TestStateHandler -> IO TestStateHandler)
mb_handler_init =
Just $ \user state -> do
let usr_id = userId user
return state{handle_curr_user = Just usr_id}
-- | Finalization of the handler, clearing the current user
mb_handler_finalize :: Maybe (TestStateHandler -> IO TestStateHandler)
mb_handler_finalize =
Just $ \state -> do
return state{handle_curr_user = Nothing}
-- ** Process Handlers
-- | Process handlers to be used in tests
processHandlers :: ProcessHandlers
processHandlers =
defaultProcessHandlers
{ listResourcesHandler = Just processListResourcesHandler
, readResourceHandler = Just processReadResourceHandler
, listToolsHandler = Just processListToolsHandler
, callToolHandler = Just processCallToolHandler
, listPromptsHandler = Just processListPromptsHandler
, getPromptHandler = Just processGetPromptHandler
, listResourceTemplatesHandler = Just processListResourceTemplatesHandler
, completeHandler = Just processCompleteHandler
}
where
-- Example process handler for listing resources
processListResourcesHandler :: ListResourcesParams -> MCPServerT (ProcessResult ListResourcesResult)
processListResourcesHandler =
return $
return $
ProcessSuccess $
ListResourcesResult
{ resources = availableResources
, nextCursor = Nothing
, _meta = Nothing
}
-- Example process handler for reading a resource
processReadResourceHandler :: ReadResourceParams -> MCPServerT (ProcessResult ReadResourceResult)
processReadResourceHandler (ReadResourceParams{uri = req_uri}) = do
let rsc_content =
case req_uri of
"resource://example/document" ->
Just $
TextResource $
TextResourceContents
{ uri = req_uri
, text = csvTestData
, mimeType = Just "text/plain"
, _meta = Nothing
}
"resource://example/data" ->
Just $
TextResource $
TextResourceContents
{ uri = req_uri
, text = jsonTestData
, mimeType = Just "application/json"
, _meta = Nothing
}
"resource://example/image" ->
Just $
BlobResource $
BlobResourceContents
{ uri = req_uri
, blob = imageTestData
, mimeType = Just "image/png"
, _meta = Nothing
}
_ -> Nothing
case rsc_content of
Just rsc_content' ->
return $ ProcessSuccess (ReadResourceResult{contents = [rsc_content'], _meta = Nothing})
Nothing ->
return $ ProcessRPCError 404 "Resource not found"
-- Example process handler for listing tools
processListToolsHandler :: ListToolsParams -> MCPServerT (ProcessResult ListToolsResult)
processListToolsHandler =
return $
return $
ProcessSuccess $
ListToolsResult
{ tools = availableTools
, nextCursor = Nothing
, _meta = Nothing
}
-- Example process handler for calling tools
processCallToolHandler :: CallToolParams -> MCPServerT (ProcessResult CallToolResult)
processCallToolHandler (CallToolParams t_name t_args) = do
case t_name of
"addition-tool" -> case t_args of
Just args_map -> do
let parseInt v =
case fromJSON @Int v of
Aeson.Success i -> Just i
Aeson.Error _ -> Nothing
let mb_arg1 = Map.lookup "arg1" args_map >>= parseInt
let mb_arg2 = Map.lookup "arg2" args_map >>= parseInt
case (mb_arg1, mb_arg2) of
(Just arg1, Just arg2) -> do
let t_result = arg1 + arg2
let result_value = Map.fromList [("result", toJSON t_result)]
let ctx_result =
TextBlock $ TextContent "text" ("The result is: " <> T.pack (show t_result)) Nothing Nothing
return $ ProcessSuccess (CallToolResult [ctx_result] (Just result_value) Nothing Nothing)
_ -> return $ ProcessRPCError 400 "addition-tool: Missing or invalid arguments"
Nothing -> return $ ProcessRPCError 400 "addition-tool: Missing arguments"
"constant-msg-tool" -> do
let t_result = "Hello, World!" :: Text
let result_value = Map.fromList [("result", toJSON t_result)]
let ctx_result =
TextBlock $ TextContent "text" ("This is a constant message: " <> t_result) Nothing Nothing
return $ ProcessSuccess (CallToolResult [ctx_result] (Just result_value) Nothing Nothing)
_ -> return $ ProcessRPCError 404 "Tool not found"
-- Example list prompts handler
processListPromptsHandler :: ListPromptsParams -> MCPServerT (ProcessResult ListPromptsResult)
processListPromptsHandler =
return $
return $
ProcessSuccess $
ListPromptsResult
{ prompts = availablePrompts
, nextCursor = Nothing
, _meta = Nothing
}
-- Example get prompt handler
processGetPromptHandler :: GetPromptParams -> MCPServerT (ProcessResult GetPromptResult)
processGetPromptHandler = \case
GetPromptParams{name = "code-review", arguments = Just map_args} -> do
let msgs =
[
( User
, T.unlines $
[ "Help me revise the syntax of the following python code snippet:"
, "```python"
, Map.findWithDefault "" "code" map_args
, "```"
]
)
, (Assistant, "Yes, I can help with that. I will make sure to check common issues and keep practices in mind.")
]
let toMsg msg_role msg_content =
PromptMessage
{ role = msg_role
, content =
TextBlock $
TextContent
{ textType = "text"
, text = msg_content
, annotations = Nothing
, _meta = Nothing
}
}
return $
ProcessSuccess $
GetPromptResult
{ description = Just "A prompt to request a code review from the LLM"
, messages = fmap (uncurry toMsg) msgs
, _meta = Nothing
}
_ -> return $ ProcessRPCError 404 "Prompt not found"
-- Resource templates handler
processListResourceTemplatesHandler :: ListResourceTemplatesParams -> MCPServerT (ProcessResult ListResourceTemplatesResult)
processListResourceTemplatesHandler =
return $
return $
ProcessSuccess $
ListResourceTemplatesResult
{ resourceTemplates = availableResourceTemplates
, nextCursor = Nothing
, _meta = Nothing
}
-- Completion handler: prefix-based completions for the code-review prompt's "code" argument
processCompleteHandler :: CompleteParams -> MCPServerT (ProcessResult CompleteResult)
processCompleteHandler (CompleteParams cref (CompletionArgument arg_name arg_value) _ctx) =
case cref of
PromptRef (PromptReference _ "code-review" _) ->
case arg_name of
"code" ->
let suggestions = filter (T.isPrefixOf arg_value) completionValues
in return $
ProcessSuccess $
CompleteResult
{ completion =
CompletionResult
{ values = suggestions
, total = Just (length suggestions)
, hasMore = Just False
}
, _meta = Nothing
}
_ -> emptyCompletion
_ -> emptyCompletion
where
emptyCompletion =
return $
ProcessSuccess $
CompleteResult
{ completion = CompletionResult{values = [], total = Just 0, hasMore = Just False}
, _meta = Nothing
}
-- | Completion suggestion values for the code-review prompt
completionValues :: [Text]
completionValues = ["def foo():", "class MyClass:", "import os"]
-- ** Available Tools
-- | Tools available in the test server
availableTools :: [Tool]
availableTools =
[ Tool
{ name = "addition-tool"
, title = Just "Addition Tool"
, description = Just "An example tool that adds two numbers"
, inputSchema =
InputSchema
{ schemaType = "object"
, properties =
Just $
Map.fromList
[ ("arg1", object ["type" .= ("number" :: Text)])
, ("arg2", object ["type" .= ("number" :: Text)])
]
, required = Just ["arg1", "arg2"]
}
, outputSchema =
Just $
InputSchema
{ schemaType = "object"
, properties = Just $ Map.fromList [("result", object ["type" .= ("number" :: Text)])]
, required = Just ["result"]
}
, annotations = Nothing
, _meta = Nothing
}
, Tool
{ name = "constant-msg-tool"
, title = Just "Constant Message Tool"
, description = Just "Another example tool"
, inputSchema =
InputSchema
{ schemaType = "object"
, properties = Nothing
, required = Nothing
}
, outputSchema =
Just $
InputSchema
{ schemaType = "object"
, properties = Just (Map.fromList [("message", object ["type" .= ("string" :: Text)])])
, required = Just ["message"]
}
, annotations = Nothing
, _meta = Nothing
}
]
-- ** Available Prompts
-- | Prompts available in the test server
availablePrompts :: [Prompt]
availablePrompts =
[ Prompt
{ name = "code-review"
, title = Just "Code Review Prompt"
, description = Just "Asks the LLM to analyze code quality and suggest improvements"
, arguments =
Just
[ PromptArgument
{ name = "code"
, title = Just "Code Snippet"
, description = Just "The code snippet to review"
, required = Just True
}
]
, _meta = Nothing
}
]
-- ** Available Resources
-- | Resources available in the test server
availableResources :: [Resource]
availableResources =
[ Resource
{ uri = "resource://example/document"
, name = "text-document"
, title = Just "An example resource for testing"
, description = Just "This is a sample text document."
, mimeType = Just "text/plain"
, size = Nothing
, annotations = Nothing
, _meta = Nothing
}
, Resource
{ uri = "resource://example/data"
, name = "json-data"
, title = Just "Example Resource 2"
, description = Just "Another example resource for testing"
, mimeType = Just "application/json"
, size = Nothing
, annotations = Nothing
, _meta = Nothing
}
, Resource
{ uri = "resource://example/image"
, name = "image-resource"
, title = Just "An inspiring image"
, description = Just "Image of a sunset"
, mimeType = Just "image/png"
, size = Nothing
, annotations = Nothing
, _meta = Nothing
}
]
-- ** Available Resource Templates
-- | Resource templates available in the test server
availableResourceTemplates :: [ResourceTemplate]
availableResourceTemplates =
[ ResourceTemplate
{ name = "user-profile"
, title = Just "User Profile Template"
, uriTemplate = "resource://example/users/{userId}"
, description = Just "Returns a user profile by ID"
, mimeType = Just "application/json"
, annotations = Nothing
, _meta = Nothing
}
, ResourceTemplate
{ name = "log-file"
, title = Just "Log File Template"
, uriTemplate = "resource://example/logs/{date}"
, description = Just "Returns logs for a specific date"
, mimeType = Just "text/plain"
, annotations = Nothing
, _meta = Nothing
}
]
-- *** Sample Resource Contents
-- | Sample CSV data for testing
csvTestData :: Text
csvTestData = "name,age,city\nJohn,25,New York\nJane,30,London\nBob,35,Paris"
-- | Sample JSON data for testing
jsonTestData :: Text
jsonTestData =
T.pack $
show $
object ["key1" .= ("value1" :: Text), "key2" .= (42 :: Int), "key3" .= (True :: Bool)]
-- | Sample image data for testing (base64 encoded)
imageTestData :: Text
imageTestData = "<base64-encoded-image-data>"
-- ** Server State Creation
-- | Create test server state for MCP tests
createTestServerState :: IO (MVar MCPServerState)
createTestServerState = do
let impl = Implementation "test-server" "1.0.0" Nothing
let server_caps =
ServerCapabilities
{ logging = Just LoggingCapability
, prompts = Just (PromptsCapability{listChanged = Nothing})
, resources =
Just
(ResourcesCapability{listChanged = Nothing, subscribe = Nothing})
, tools = Just (ToolsCapability{listChanged = Just True})
, completions = Just CompletionsCapability
, experimental = Nothing
}
newMVar
MCPServerState
{ mcp_server_initialized = False
, mcp_handler_state = initializeTestState
, mcp_handler_init = mb_handler_init
, mcp_handler_finalize = mb_handler_finalize
, mcp_client_capabilities = Nothing
, mcp_log_level = Just Info -- Set to Debug if you want to see json requests
, mcp_pending_responses = mempty
, mcp_pending_responses_next = 1
, mcp_server_capabilities = server_caps
, mcp_implementation = impl
, mcp_instructions = Nothing
, mcp_process_handlers = processHandlers
}
-- ** Application Setup
-- | Create test application with access to JWT configuration
createTestAppWithJWT :: IO (AuthServer.JWTSettings, Application)
createTestAppWithJWT = do
state_var <- createTestServerState
key <- AuthServer.generateKey
let jwt_cfg = AuthServer.defaultJWTSettings key
let cookie_cfg = AuthServer.defaultCookieSettings
let auth_cfg = cookie_cfg :. jwt_cfg :. EmptyContext
let app = serveWithContext (Proxy @MCPAPI) auth_cfg (mcpAPI state_var)
return (jwt_cfg, app)