13 KiB
Agent Fox - API Documentation MCP Service
Context
Developers using LLMs (Claude, GPT, etc.) often need to reference API documentation while coding. Currently, they either paste entire API docs into the context (wasting tokens) or manually copy relevant sections. Agent Fox solves this by providing an MCP service that lets LLMs efficiently query API documentation through multi-level retrieval, minimizing token consumption while maximizing usefulness.
Target: Developer-facing SaaS product with user authentication and multi-tenant isolation.
Architecture Overview
Monorepo Structure (pnpm workspace)
agent-fox/
├── packages/
│ ├── web/ # React frontend (Vite + TailwindCSS + shadcn/ui)
│ ├── server/ # Express backend API
│ ├── mcp/ # MCP service (independent Express process)
│ └── shared/ # Shared types + Prisma client
├── prisma/ # Prisma schema + migrations
├── package.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
serverandmcpare independently deployable processes sharing the same PostgreSQL database via thesharedPrisma client.webis a static SPA served separately (or via CDN).
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite + TailwindCSS + shadcn/ui + React Router + TanStack Query |
| Backend API | Express + TypeScript + Zod (validation) |
| MCP Service | @modelcontextprotocol/server + @modelcontextprotocol/express + @modelcontextprotocol/node |
| Database | PostgreSQL + Prisma ORM |
| OpenAPI Parsing | @apidevtools/swagger-parser |
| Auth | JWT (access + refresh) + bcrypt + Passport.js (GitHub/Google OAuth) |
| Language | TypeScript throughout |
Data Model
User
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| String | Unique, for email/password auth | |
| passwordHash | String? | Nullable for OAuth-only users |
| name | String | Display name |
| avatarUrl | String? | Profile picture |
| createdAt | DateTime | |
| updatedAt | DateTime |
OAuthAccount
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| userId | UUID | FK → User |
| provider | String | "github" or "google" |
| providerAccountId | String | External account ID |
| createdAt | DateTime |
Project
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key, exposed as project ID |
| userId | UUID | FK → User (owner) |
| name | String | Project display name |
| description | String? | Optional description |
| baseUrl | String? | API base URL |
| openApiSpec | JSONB | Full dereferenced OpenAPI document |
| openApiVersion | String | e.g., "3.0.3", "3.1.0" |
| apiKeyHash | String | Hashed API key for MCP access |
| createdAt | DateTime | |
| updatedAt | DateTime |
Module
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| projectId | UUID | FK → Project |
| name | String | Module name (from tag or path prefix) |
| description | String? | Module description |
| sortOrder | Int | Display order |
| source | Enum | "tag", "path_prefix", "manual" |
| createdAt | DateTime | |
| updatedAt | DateTime |
Endpoint
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| projectId | UUID | FK → Project |
| moduleId | UUID | FK → Module |
| method | String | HTTP method (GET, POST, PUT, DELETE, etc.) |
| path | String | URL path (e.g., /api/users/{id}) |
| summary | String? | Short description |
| description | String? | Detailed description |
| operationId | String? | OpenAPI operationId |
| parameters | JSONB | Path, query, header parameters |
| requestBody | JSONB? | Request body schema |
| responses | JSONB | Response schemas by status code |
| tags | String[] | Original OpenAPI tags |
| deprecated | Boolean | Whether the endpoint is deprecated |
| createdAt | DateTime | |
| updatedAt | DateTime |
MCP Multi-Level Retrieval Design (Core Feature)
The MCP service exposes 5 tools that enable LLMs to progressively drill down into API documentation, minimizing token usage at each step.
Tool Definitions
1. get_project_overview
- Description (shown to LLM): "Get an overview of this API project including its name, version, base URL, and a summary of available modules with endpoint counts. Call this first to understand what the API offers. This is usually sufficient to decide which module to drill into."
- Input: (none — projectId comes from the MCP connection URL)
- Output:
{ name, description, version, baseUrl, totalEndpoints, modules: [{ id, name, endpointCount }] } - Estimated tokens: ~200
- Note: This is the recommended entry point. It provides a compact overview including module names and counts, enough for LLMs to decide next steps.
2. list_modules
- Description: "List all API modules/groups with their descriptions. Each module contains related endpoints. Use this when you need module descriptions to decide which module to explore."
- Input: (none)
- Output:
[{ id, name, description, endpointCount }] - Estimated tokens: ~100-300
- Note: Differs from
get_project_overviewby including module descriptions. Use when the module name alone isn't enough to determine relevance.
3. list_endpoints
- Description: "List all endpoints in a specific module. Returns method, path, and summary for each endpoint. Use get_endpoint_detail to get full information about a specific endpoint."
- Input:
{ moduleId: string } - Output:
[{ id, method, path, summary, deprecated }] - Estimated tokens: ~200-500
4. get_endpoint_detail
- Description: "Get complete details for a specific endpoint including parameters, request body schema, response schemas, and examples. Use this when you need to understand exactly how to call an endpoint."
- Input:
{ endpointId: string } - Output:
{ method, path, summary, description, parameters, requestBody, responses, deprecated } - Estimated tokens: ~500-2000
5. search_endpoints
- Description: "Search for endpoints by keyword. Searches across path, summary, description, operationId, and parameter names. Optionally filter by module. Returns matching endpoint summaries."
- Input:
{ keyword: string, moduleId?: string } - Output:
[{ id, method, path, summary, moduleName, deprecated }] - Estimated tokens: ~200-500
Retrieval Flow Example
LLM wants to call "create user" endpoint:
1. get_project_overview() → ~200 tokens (see all modules)
2. list_endpoints({ moduleId: "users" }) → ~300 tokens (see user endpoints)
3. get_endpoint_detail({ endpointId: "..." }) → ~800 tokens (get full details)
Total: ~1,300 tokens (vs 10,000+ for full doc dump)
MCP Authentication
- MCP endpoint URL:
https://host/mcp/:projectId - Auth via
Authorization: Bearer <project-api-key>header - API key is validated against the project's
apiKeyHash - Each project has its own isolated MCP instance
MCP Transport
Support both transport protocols:
- Streamable HTTP (new standard): POST/GET/DELETE on
/mcp/:projectId - SSE (legacy): GET with SSE on
/mcp/:projectId/sse, POST on/mcp/:projectId/messages
Use @modelcontextprotocol/express middleware with session management for stateful connections.
Frontend Pages
1. Auth Pages
- Login: email/password form + GitHub/Google OAuth buttons
- Register: email/password form + OAuth
2. Projects List Page
- Card grid of user's projects
- Each card: project name, version, endpoint count, created date
- Create project button → import flow
3. Project Detail Page (Tabbed)
Tab: Documentation Preview
- Interactive API doc browser (similar to Swagger UI)
- Left sidebar: module list (collapsible)
- Main area: endpoint list grouped by module, expandable to show details
- Supports try-it-out (optional, future feature)
Tab: Module Management
- View auto-generated modules
- Drag-and-drop reorder
- Move endpoints between modules
- Create/rename/delete modules
Tab: MCP Integration
- MCP service URL (copyable)
- API Key display (masked, with copy and rotate buttons)
- Configuration snippet for Claude Code, Cursor, etc. (copyable JSON)
- Connection status indicator
Tab: Settings
- Project name/description editing
- Re-import OpenAPI document (with diff preview)
- Danger zone: delete project
Import Flow
- User uploads JSON/YAML file OR pastes URL
- Backend validates with
@apidevtools/swagger-parser - Backend dereferences all
$refpointers - Parses tags → Module records, paths → Endpoint records
- Endpoints without tags auto-grouped by path prefix (first segment)
- Preview shown to user with module/endpoint breakdown
- User confirms → data saved
Backend API
Auth Routes
POST /api/auth/register # Email registration
POST /api/auth/login # Email login → returns JWT pair
POST /api/auth/refresh # Refresh access token
GET /api/auth/github # GitHub OAuth redirect
GET /api/auth/google # Google OAuth redirect
GET /api/auth/callback/:provider # OAuth callback
Project Routes
GET /api/projects # List user's projects
POST /api/projects # Create project (upload OpenAPI doc)
GET /api/projects/:id # Get project details
PUT /api/projects/:id # Update project metadata
DELETE /api/projects/:id # Delete project
Module/Endpoint Routes
GET /api/projects/:id/modules # List modules
PUT /api/projects/:id/modules/:mid # Update module (rename, reorder)
POST /api/projects/:id/modules # Create manual module
DELETE /api/projects/:id/modules/:mid # Delete module
PATCH /api/projects/:id/endpoints/:eid # Move endpoint to different module
Import/Key Routes
POST /api/projects/:id/reimport # Re-import OpenAPI document
POST /api/projects/:id/api-key/rotate # Rotate API key
Authentication Design
- User auth: JWT dual-token (access: 15min, refresh: 7d)
- Password: bcrypt hashed
- OAuth: Passport.js strategies for GitHub and Google
- MCP auth: Project-level API key (independent from user JWT)
- API key format:
afk_prefix + 32-char random string - API key stored as bcrypt hash in database
Deployment (Docker Compose)
All services containerized and orchestrated via Docker Compose for one-command deployment.
Services
services:
postgres: # PostgreSQL 16
server: # Backend API (Express) - port 3000
mcp: # MCP service (Express) - port 3001
web: # Frontend (Nginx serving static build) - port 80
redis: # Optional: session store / rate limiting cache
Container Details
| Service | Base Image | Notes |
|---|---|---|
postgres |
postgres:16-alpine |
Persistent volume for data, init scripts for DB creation |
server |
node:20-alpine |
Multi-stage build: build → runtime only |
mcp |
node:20-alpine |
Same multi-stage build pattern |
web |
node:20-alpine → nginx:alpine |
Build stage + Nginx serve stage |
redis |
redis:7-alpine |
Optional, for rate limiting and session cache |
Docker Files
agent-fox/
├── docker-compose.yml # Orchestration
├── docker-compose.dev.yml # Dev overrides (hot reload, debug ports)
├── packages/
│ ├── web/Dockerfile
│ ├── server/Dockerfile
│ └── mcp/Dockerfile
└── .env.example # Environment variable template
Environment Variables
Managed via .env file (git-ignored), with .env.example as template:
DATABASE_URL— PostgreSQL connection stringJWT_SECRET— JWT signing keyGITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET— GitHub OAuthGOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET— Google OAuthMCP_BASE_URL— Public URL for MCP serviceREDIS_URL— Optional Redis connection
Dev vs Prod
- Dev (
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up): Source mounted as volumes, hot reload enabled, debug ports exposed - Prod (
docker-compose up): Optimized multi-stage builds, no source mounting, Nginx serves frontend
Error Handling
- All API responses follow
{ success: boolean, data?: T, error?: { code, message } }format - Zod validation on all inputs
- MCP tools return structured error messages that help LLMs self-correct
- Rate limiting on MCP endpoints to prevent abuse
Verification Plan
Unit Tests
- OpenAPI parsing: validate correct module/endpoint extraction from sample docs
- MCP tools: verify each tool returns correct data shape and respects scoping
- Auth: test JWT generation, validation, refresh flow
Integration Tests
- Full import flow: upload OpenAPI doc → verify modules/endpoints created correctly
- MCP retrieval flow: simulate LLM calling tools in sequence
- Auth flow: register → login → access protected routes
Manual Testing
- Import Petstore OpenAPI sample → verify preview and module grouping
- Configure Claude Code with generated MCP config → verify tools work
- Test search with various keywords → verify relevance