AI MCP Read API — Overview
An internal HTTP service running in guinness-ai-v2 that exposes DocumentDB design and code data (design / code collections) to the MCP Server. It provides separate list/detail endpoints for design and code, protected by service-to-service authentication (X-AI-Service-Token). Deployed as a Lambda Function URL or private HTTP integration accessible only within the VPC; no external access is possible.
Queue: None — HTTP only (triggered by HTTP GET requests from the MCP Server)
Maximum execution time: Under 5 seconds per request
Source: guinness-ai-v2/apps/mcp-api/ (new app)
1. Tech Stack
| Layer |
Technology |
| Runtime |
Python 3.12 on AWS Lambda |
| AI Framework |
None — pure data retrieval, no LLM calls |
| Database |
Amazon DocumentDB (MongoDB-compatible) — design / code collections, owned by AI |
| Storage |
None — no S3 access |
| Queue |
None — HTTP only |
| Authentication |
X-AI-Service-Token header (shared secret, constant-time comparison) |
| Deployment |
Lambda Function URL, VPC-internal only; security group allows inbound from MCP Lambda only |
| RDB |
No access — does not touch PostgreSQL (database isolation is respected) |
2. Why This App Exists
Replaces direct DocumentDB access from the MCP Server and enforces database isolation: backend owns Postgres, AI owns DocumentDB.
| Item |
Before (V1) |
After (V2) |
| DocumentDB reads from MCP |
Direct connection from TypeScript MCP |
HTTP GET to this API |
| DB isolation |
Violated — backend reads AI's DocumentDB |
Respected — backend calls HTTP, AI reads its own DB |
| Deployment coupling |
MCP requires DocumentDB TLS certs + connection |
MCP only needs an HTTP client + service token |
3. Request Flow
flowchart TD
A["MCP Server<br/>(guinness-backend)"] -->|"HTTP GET /internal/designs or /internal/code<br/>X-AI-Service-Token"| B["認証ミドルウェア<br/>(定数時間比較)"]
B -->|無効| C["401 Unauthorized"]
B -->|有効| D{"クエリパラメータ<br/>でルーティング"}
D -->|/internal/designs| E["design.find / find_one"]
D -->|/internal/code| F["code.find / find_one"]
E --> G["レスポンスエンベロープ構築"]
F --> H["合計件数取得<br/>ページネーションレスポンス構築"]
G --> I["{ ok: true, data: {...} }"]
H --> I
E -->|見つからない| J["{ ok: false, error: NOT_FOUND }"]
style A fill:#f9f,stroke:#333
style B fill:#bbf,stroke:#333
style I fill:#bfb,stroke:#333
style C fill:#fbb,stroke:#333
style J fill:#fbb,stroke:#333
4. DocumentDB Query Patterns
Four read-only query patterns are selected by path. None returns vector embeddings; every vector_embedding field is excluded from projections.
Pattern 1: List designs by project_id
filter = {"project_id": pid}
if organization_id is not None:
filter["organization_id"] = organization_id
design_collection.find(filter).skip(offset).limit(limit)
| Property |
Value |
| Endpoint |
GET /internal/designs?project_id=... |
| Collection |
DESIGN_TABLE_NAME (env var, default design) |
| Projection |
_id, organization_id, project_id, name, visual.metadata.image_url, non-vector metadata |
| When empty |
Returns { ok: true, data: { total: 0, items: [] } } |
Pattern 2: Lookup design by design_id
design_collection.find_one({"_id": design_id})
| Property |
Value |
| Collection |
DESIGN_TABLE_NAME (env var, default design) |
| Operation |
find_one |
| Filter |
{"_id": design_id} |
| Returned fields |
design document metadata without vector_embedding fields |
When None |
Returns { ok: false, error: { code: "NOT_FOUND" } } |
Pattern 3: List code by project_id
filter = {"project_id": pid}
if organization_id is not None:
filter["organization_id"] = organization_id
total = code_collection.count_documents(filter)
cursor = (
code_collection.find(filter)
.projection({
"_id": 1,
"name": 1,
"visual.metadata.image_url": 1,
})
.skip(offset)
.limit(limit)
)
| Property |
Value |
| Collection |
CODE_TABLE_NAME (env var, default code) |
| Operation |
find + count_documents |
| Filter |
{"project_id": pid} plus organization_id when provided |
| Projection |
_id, organization_id, project_id, name, type, based_on, visual.metadata.image_url |
| Pagination |
.skip(offset).limit(limit) |
| Defaults |
limit=20, offset=0 |
Max limit |
100 (hard cap) |
| When empty |
Returns { ok: true, data: { total: 0, items: [] } } — not an error |
Pattern 4: Lookup code by code_id
code_collection.find_one({"_id": code_id})
| Property |
Value |
| Endpoint |
GET /internal/code/{code_id} |
| Collection |
CODE_TABLE_NAME (env var, default code) |
| Operation |
find_one |
| Filter |
{"_id": code_id} |
| Returned fields |
code document metadata, source_code, and css_code; no vector_embedding fields |
When None |
Returns { ok: false, error: { code: "NOT_FOUND" } } |
5. Deployment Model
flowchart LR
subgraph VPC["VPC (ap-northeast-1)"]
MCP["MCP Lambda<br/>SG: lambda-mcp"]
API["AI Read API Lambda<br/>SG: lambda-mcp-api<br/>Function URL (VPC 内部)"]
DOCDB[(DocumentDB<br/>design + code コレクション)]
end
MCP -- "HTTP GET<br/>X-AI-Service-Token" --> API
API -- "クエリ" --> DOCDB
| Property |
Value |
| Runtime |
Python 3.12 |
| Handler |
Lambda Function URL (no API Gateway, no CloudFront) |
| Network |
VPC-internal only — cannot be called from outside |
| Security group inbound |
MCP Lambda SG only |
| Security group outbound |
DocumentDB SG |
| Memory |
256 MB (read-only, no heavy processing) |
| Timeout |
30 seconds |
6. Response Envelope
All responses (success and error) use a consistent JSON envelope:
// Success
{
"ok": true,
"data": { ... },
"error": null
}
// Error
{
"ok": false,
"data": null,
"error": { "code": "NOT_FOUND", "message": "Design not found" }
}
The MCP Server converts this envelope into an MCP tool response:
- ok: true → structuredContent in the MCP response
- ok: false → isError: true in the MCP response
7. Authentication
Service-to-service authentication using a shared secret:
flowchart LR
MCP["MCP Server"] -->|"X-AI-Service-Token: <secret>"| API["AI Read API"]
API -->|"hmac.compare_digest<br/>(定数時間)"| ENV["AI_SERVICE_TOKEN<br/>環境変数"]
| Property |
Value |
| Header name |
X-AI-Service-Token |
| Validation |
hmac.compare_digest(token, AI_SERVICE_TOKEN) — constant-time comparison |
| On failure |
HTTP 401, { ok: false, error: { code: "UNAUTHORIZED" } } |
| Token source |
Shared secret — same value set in env vars for both MCP Lambda and AI Read API Lambda |
| Infrastructure |
random_password Terraform resource or AWS Secrets Manager |
8. Module Structure
apps/mcp-api/src/mcp_api/
handler.py # Lambda entry point — HTTP handler (Function URL event parsing)
router.py # Route definitions — /internal/designs and /internal/code
service.py # Business logic — parameter validation, response construction
repo.py # DocumentDB I/O — design / code collection queries
schemas.py # Request/response Pydantic models
config.py # pydantic-settings — AI_SERVICE_TOKEN, DocumentDB connection
Reused from packages:
- packages/utils/documentdb.py — DocumentDB connection helper (lazy initialization, reused between invocations)
- packages/observability/ — Structured JSON logging (init_logger("ai-mcp"))
9. Environment Variables
| Variable |
Description |
Example / Default |
AI_SERVICE_TOKEN |
Shared secret for internal authentication |
(Terraform-generated) |
DOCUMENTDB_CONNECTION_STRING |
DocumentDB connection string |
mongodb://user:pass@host:27017/?tls=true |
DOCUMENTDB_NAME |
DocumentDB database name |
guinness_v2 |
DESIGN_TABLE_NAME |
design collection name |
design |
CODE_TABLE_NAME |
code collection name |
code |