v1.2.2 — 2026-06-07
MCP (Model Context Protocol) server for Curry Scheme. Exposes Scheme functions as MCP tools and resources callable by Claude Code and other MCP clients. Two transports: stdio (one client, spawned per session) and SSE (HTTP + Server-Sent Events, many concurrent clients).
Authentication is optional and supports three OAuth 2.0 modes — see
docs/guides/mcp-authentication.md for
worked examples.
(import (curry mcp))The MCP module requires OpenSSL (used for auth and UUID generation).
Enable it with -DBUILD_MODULE_MCP=ON when configuring CMake.
Register a callable tool. Must be called before mcp-serve / mcp-serve-sse.
| Argument | Type | Description |
|---|---|---|
name |
string | Unique tool name shown to the LLM |
description |
string | Human-readable description of what the tool does |
schema |
alist | Parameter schema (see below) |
handler |
procedure | Called with an alist of (symbol . value) pairs |
Schema format — alist mapping parameter names (symbols or strings) to property alists:
'((param-name . ((type . "string")
(description . "What this param does")
(default . "optional default value"))))Parameters without a (default . ...) entry are marked required in the
JSON Schema sent to the LLM.
Handler receives one argument: an alist of (symbol . value) pairs.
Values are decoded from JSON: string → string, integer → fixnum,
number → flonum, boolean → #t/#f, array → list, null → '().
The handler must return either an (mcp-text ...) or (mcp-json ...) value.
(mcp-tool "add" "Add two numbers"
'((a . ((type . "number") (description . "First operand")))
(b . ((type . "number") (description . "Second operand"))))
(lambda (args)
(let ((a (cdr (assq 'a args)))
(b (cdr (assq 'b args))))
(mcp-text (number->string (+ a b))))))Register a browseable resource. handler receives the URI string and must
return (mcp-text ...) or a plain string.
(mcp-resource "file:///config.json" "Server configuration"
(lambda (uri) (mcp-text (read-file "config.json"))))Wraps a string as a plain-text tool/resource result.
Wraps any Scheme value as a JSON tool result. The value is serialised to JSON
using the same rules as mcp-serve output.
Emit an MCP notifications/progress event to the client during a long-running
tool call. current and total are numbers; message is an optional status
string. Has no effect when called outside a tool handler.
(mcp-tool "slow-task" "A task that takes a while" '()
(lambda (_)
(mcp-notify-progress 0 3 "starting")
(do-step-1)
(mcp-notify-progress 1 3 "step 1 done")
(do-step-2)
(mcp-notify-progress 2 3 "step 2 done")
(do-step-3)
(mcp-text "done")))Start the stdio JSON-RPC transport. Blocks reading from stdin until EOF. Use this when Claude Code spawns the server as a subprocess.
(mcp-serve) ; server name defaults to "curry-mcp"
(mcp-serve "my-server" "1.0") ; custom name and versionClaude Code config (~/.config/claude/claude_desktop_config.json):
{
"mcpServers": {
"my-server": {
"command": "/path/to/build/curry",
"args": ["/path/to/my_server.scm"]
}
}
}Start the HTTP + SSE transport. Blocks forever; spawns one thread per incoming connection.
Endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/sse |
Open SSE event stream; emits event: endpoint with the POST URL |
POST |
/message?sessionId=<id> |
Submit a JSON-RPC request; response arrives via SSE |
POST |
/token |
Issue an access token (self-contained auth mode only) |
OPTIONS |
any | CORS preflight — always allowed without auth |
(mcp-serve-sse 8080)
(mcp-serve-sse 8080 "my-server")
(mcp-serve-sse 8080 "my-server" "2.1.0")Prints [mcp] SSE server listening on port N to stderr when ready.
Claude Code config (persistent SSE server):
{
"mcpServers": {
"my-server": {
"url": "http://localhost:8080/sse"
}
}
}All auth procedures must be called before mcp-serve-sse. Auth is not
applied to the stdio transport.
Select the authentication mode. mode is one of:
| Symbol | Behaviour |
|---|---|
'none |
No auth — any client may connect (default) |
'self-contained |
Server issues opaque tokens via POST /token (RFC 6749 §4.4) |
'introspect |
Validate tokens via an external RFC 7662 introspection endpoint |
'jwt |
Validate JWT Bearer tokens locally with OpenSSL (RFC 7519) |
The server acts as its own authorisation server.
Register a client. Clients exchange their id + secret for an access token
via POST /token. Call multiple times to register multiple clients.
Set the access token lifetime in seconds. Default: 3600.
Token endpoint:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<id>&client_secret=<secret>
Success response (200 OK):
{ "access_token": "<uuid>", "token_type": "Bearer", "expires_in": 3600 }Error responses follow RFC 6749 §5.2:
| HTTP | error field |
Cause |
|---|---|---|
| 400 | unsupported_grant_type |
grant_type ≠ client_credentials |
| 401 | invalid_client |
Unknown client ID or wrong secret |
| 503 | server_error |
Token store full |
URL of the identity provider's RFC 7662 introspection endpoint.
Both http:// and https:// are supported.
Optional. Send HTTP Basic authentication to the introspection endpoint using these resource-server credentials.
How long to cache introspection results in process memory. Default: 60.
Increase for performance; decrease for tighter revocation detection.
Introspection request made by the server for each uncached token:
POST <introspection-url>
Authorization: Basic <base64(id:secret)> ; if credentials configured
Content-Type: application/x-www-form-urlencoded
token=<bearer-token>
The server checks that the response contains "active": true.
Select the signature algorithm. alg is one of:
| Symbol | Algorithm | Key type |
|---|---|---|
'hs256 |
HMAC-SHA-256 | Shared secret string |
'rs256 |
RSA-SHA-256 | RSA public key (PEM) |
Default: 'hs256.
Set the HMAC secret for HS256. Should be at least 32 random bytes.
Load an RS256 public key from a PEM file at path.
Load an RS256 public key from an inline PEM string.
Optional. If set, the JWT iss claim must equal iss.
Optional. If set, the JWT aud claim must equal aud.
JWT validation steps performed:
- Split token into
header.payload.signature - Verify the signature matches the algorithm and key
- Check
expis in the future - Check
issifmcp-jwt-issuer!was called - Check
audifmcp-jwt-audience!was called
Tokens failing any check are rejected with HTTP 401.
All auth failures return:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp"[, error="...", error_description="..."]
Content-Length: 0
The error and error_description fields follow RFC 6750 §3.1:
error |
When |
|---|---|
| (none) | No token was sent at all |
invalid_token |
Token present but invalid, expired, or not recognised |
; Full parameter schema example
'((query . ((type . "string")
(description . "Search terms")))
(limit . ((type . "integer")
(description . "Max results (optional)")
(default . 10)))
(debug . ((type . "boolean")
(description . "Enable verbose output")
(default . #f))))Supported type values: "string", "number", "integer", "boolean",
"array", "object".
; Get a required parameter (raises if missing)
(define (arg args name) (cdr (assq name args)))
; Get an optional parameter with a default
(define (arg? args name default)
(let ((p (assq name args))) (if p (cdr p) default)))
; Usage in a handler:
(lambda (args)
(let ((q (arg args 'query))
(limit (arg? args 'limit 10)))
(mcp-text (search q limit))))- stdio — newline-delimited JSON-RPC 2.0; one client only; synchronous dispatch.
- SSE — one thread per HTTP connection; up to 32 concurrent sessions; tool calls serialised by a mutex (Scheme evaluator is single-threaded); keepalive comments sent every 15 s; up to 1 MB per JSON-RPC message.
- Scheme global state persists across tool calls for the lifetime of the server process.
(self)is not available from tool handlers (main thread is not an actor).- Tool exceptions are caught and returned as JSON-RPC error responses
(
code: -32603); the server continues running normally. - Auth tokens in
self-containedmode are stored in process memory only. They are lost on server restart.
- Authentication guide — step-by-step OAuth 2.0 setup
- Example servers —
mcp_server.scm,mcp_math.scm,mcp_nbody.scm