Effect Q&A MCP Server
by forthy
README.md
# Effect Q&A MCP Server
A Model Context Protocol (MCP) server that exposes a single tool: `get-categories-summary`. Fetches category data from a Q&A service API and returns a markdown summary grouped by category.
---
## Quick Start
```bash
# Install dependencies
bun install
# Start the MCP server (requires QA_SERVICE_URL)
QA_SERVICE_URL=https://your-qa-api.example.com bun run index.ts
# Run tests
bun run test
# Type-check
npx tsc --noEmit
# Format code
bun run fmt
```
---
## Architecture Overview
### Component Diagram
```mermaid
graph TB
subgraph "MCP Client"
Client[MCP Client<br/>(Claude, Cursor, etc.)]
end
subgraph "MCP Server Process"
Server[McpServer.layerHttp<br/>:3000/mcp]
Tool[get-categories-summary<br/>Tool Handler]
ApiClient[ApiClient Service<br/>fetchCategories]
HttpClient[NodeHttpClient.layer<br/>Platform HTTP]
end
subgraph "External"
QA_API[Q&A Service API<br/>GET /api/v1/categories]
end
Client <-- MCP over HTTP --> Server
Server --> Tool
Tool --> ApiClient
ApiClient --> HttpClient
HttpClient --> QA_API
```
### Layer Stack (Inner → Outer)
The server is composed as an Effect `Layer` stack. Each layer provides services to the layers above it.
```mermaid
graph TB
subgraph "Layer Stack"
L5[Layer 5: registerTool<br/>Tool Registration]
L4[Layer 4: ApiClient.Default<br/>Q&A API Client]
L3[Layer 3: McpServer.layerHttp<br/>MCP Protocol @ /mcp]
L2[Layer 2: NodeHttpServer.layer<br/>Node HTTP Server]
L1[Layer 1: NodeHttpClient.layer<br/>Platform HTTP Transport]
end
L1 --> L2
L2 --> L3
L3 --> L4
L4 --> L5
style L1 fill:#f9f,stroke:#333
style L2 fill:#bbf,stroke:#333
style L3 fill:#bfb,stroke:#333
style L4 fill:#ff9,stroke:#333
style L5 fill:#f96,stroke:#333
```
| Layer | Responsibility | Provides |
| ------------------------- | ----------------------------------------------------------- | ----------------------- |
| 1. `NodeHttpClient.layer` | Platform HTTP transport (fetch, connections, pooling) | `HttpClient.HttpClient` |
| 2. `NodeHttpServer.layer` | Node.js HTTP server listening on `MCP_PORT` (default 3000) | `HttpServer.HttpServer` |
| 3. `McpServer.layerHttp` | MCP protocol handler at `/mcp` endpoint | `McpServer.McpServer` |
| 4. `ApiClient.Default` | Q&A API client with config (`QA_SERVICE_URL`, `QA_API_KEY`) | `ApiClient` |
| 5. `registerTool` | Wires `get-categories-summary` handler to MCP server | — (side effect) |
---
## Module Structure
```
src/
├── index.ts — MCP server setup, layer composition, entry point
├── api-client.ts — Effect.Service for Q&A API (errors, HTTP adapter)
├── summary.ts — Pure function: Category[] → markdown string
├── types.ts — Effect Schema definitions (Category, CategoriesResponse)
└── services.ts — Reserved for future top-level services
test/
├── index.test.ts — buildSummary tests + handler with nock
└── summary.test.ts — buildSummary edge cases
```
---
## Data Flow
```mermaid
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
participant Tool as get-categories-summary
participant ApiClient as ApiClient
participant HttpClient as HttpClient
participant QA as Q&A API
Client->>Server: POST /mcp {method: "tools/call", params: {name: "get-categories-summary"}}
Server->>Tool: invoke handler
Tool->>ApiClient: fetchCategories()
ApiClient->>HttpClient: execute GET /api/v1/categories
HttpClient->>QA: HTTP Request
QA-->>HttpClient: 200 OK {data: [...]}
HttpClient-->>ApiClient: HttpClientResponse
ApiClient->>ApiClient: filterStatusOk → decode JSON → Schema.decode
ApiClient-->>Tool: CategoriesResponse
Tool->>Tool: buildSummary(categories.data)
Tool-->>Server: CallToolResult {content: [TextContent]}
Server-->>Client: MCP Response
```
---
## Configuration
| Environment Variable | Required | Description |
| -------------------- | -------- | ---------------------------------------------------- |
| `QA_SERVICE_URL` | Yes | Base URL of Q&A API (e.g., `https://qa.example.com`) |
| `QA_API_KEY` | No | Bearer token for authenticated requests |
| `MCP_PORT` | No | HTTP listen port (default: `3000`) |
---
## Error Handling
The tool handler folds all errors into `CallToolResult` with `isError: true`:
| Error Type | Trigger | User-Facing Message |
| -------------- | -------------------------------- | ------------------------------------------------------------------- |
| `HttpError` | Non-2xx HTTP status | `Error fetching categories: HTTP <status>` |
| `DecodeError` | Schema validation failed | `Error fetching categories: unexpected response format — <details>` |
| `UnknownError` | Transport/DNS/timeout/JSON parse | `Error fetching categories: <message>` |
---
## Tool Specification
### `get-categories-summary`
- **Description**: Fetches categories from the Q&A service and returns a narrative summary grouped by category
- **Input**: None (empty object)
- **Annotations**: `readOnlyHint: true` — clients may cache/replay
- **Output**: Markdown string via `TextContent`
**Example Output:**
```markdown
Found 3 categories across 2 groups.
## bugs (2)
- **Login Issue**: Cannot log in
- **Crash**: App crashes on start
## features (1)
- **Dark Mode**: Add dark theme
```
---
## Testing
```bash
# Unit tests (pure functions)
bun run test
# With coverage
bun run test:coverage
```
Test strategy:
- `buildSummary` — pure function tests (empty, single, multi-category)
- Handler — integration test with `nock` intercepting HTTP to `QA_SERVICE_URL`
---
## Development
```bash
# Format check
bun run fmt:check
# Type-check
npx tsc --noEmit
# Regenerate docs
bun run docs
```
---
## Dependencies
### Runtime
- `@effect/ai` — MCP server implementation
- `@effect/platform-node` — Node.js platform layers (HTTP client/server)
- `effect` — Core Effect-TS (services, layers, schemas, errors)
### Dev
- `@effect/vitest` — Effect-native testing (`it.effect`, `assert`)
- `nock` — HTTP mocking for integration tests
- `oxfmt` — Formatter (printWidth 120, no semicolons)
- `@effect/language-service` — IDE support via tsconfig plugin
---
## License
Private — internal tooling.