Skip to main content
Glama
forthy

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.