# Project Overview
This project integrates a Model Context Protocol (MCP) server directly into Strapi 5 as a native plugin.
Originally, the MCP server was developed as a standalone Node service (`strapi-mcp-server/`).
The architecture is being refactored so that:
1. The MCP server logic runs inside Strapi as a plugin
2. It directly uses Strapi's internal Document Service
3. Reference implementation: `@sensinum/strapi-plugin-mcp` (transport & session patterns)
4. It avoids REST/GraphQL HTTP overhead
5. It supports secure end-user scoped CRUD operations
6. **Each MCP client maps to a real Strapi admin user** (no shared service account)
**Deployment: Dockerfile & docker-compose to run Strapi (with MCP plugin) in container**
See [docs/architecture.md](docs/architecture.md) and [docs/authentication-flow.md](docs/authentication-flow.md) for the current design.
---
# Repository Structure
/strap-mcp-server/ # Legacy standalone MCP server (reorg target)
/strapi-plugins/
/mcp-server/
/server/
/admin/
/src/
controllers/
services/
routes/
middlewares/
---
## Required Refactor
### 1. Standalone MCP Reorganization
Move:
- README.md
- package.json
- package-lock.json
- docs/
- scripts/
- src/
Under:
/strap-mcp-server/
Ensure:
- Clean entrypoint
- Proper build output
- No hardcoded paths
- Environment variables aligned with Strapi plugin usage
---
# Why MCP as a Strapi Plugin?
## The Internal Service Advantage
Running MCP as a separate process limits capabilities to REST/GraphQL APIs.
By embedding MCP inside Strapi 5, the plugin can:
### Direct Document Service Access
```ts
strapi.documents('api::article.article').findMany()
```
### Advantages
| Benefit | Description |
|----------|-------------|
| No HTTP Overhead | In-memory service calls |
| Bypass REST permissions layer | No token juggling |
| Schema Awareness | Access full content registry |
| Full Type Metadata | Access components and relations |
| Lower Latency | No network serialization |
This is not just about speed.
It is about architecture simplicity and capability depth.
---
# MCP Communication Flow
## Streamable HTTP Transport (MCP spec 2025-03-26)
Single endpoint: `POST/GET/DELETE /api/mcp-server/mcp`
### 1. Initialize
POST /api/mcp-server/mcp (with initialize request body)
- Creates new McpServer + StreamableHTTPServerTransport
- Returns mcp-session-id header
- User resolved from Authorization header, cached on session
### 2. Tool Invocation
POST /api/mcp-server/mcp (with mcp-session-id header)
- Routes to existing session
- Executes tool with session user context
- Returns result
### 3. Notifications (optional)
GET /api/mcp-server/mcp (with mcp-session-id header)
- SSE stream for server-to-client notifications
### 4. Terminate
DELETE /api/mcp-server/mcp (with mcp-session-id header)
- Cleans up session and transport
---
# Authentication Middleware
File: `src/middlewares/mcp-auth.ts`
## Purpose
Resolve each MCP client to a real Strapi admin user. No shared service account.
## Architecture
Each MCP client maps 1:1 to a Strapi admin user:
| Identity | Who | How resolved |
|----------|-----|-------------|
| MCP Client | AI agent / orchestrator | JWT email claim → Strapi admin user lookup |
| Strapi Admin User | The resolved identity | `ctx.state.user` for all Document Service calls |
The resolved user's ID becomes the `createdBy`/`updatedBy` on all records. Native Strapi ownership works.
## Final Architecture
```
End User
↓ (SSO Login)
Agent Orchestrator
↓ (Signed JWT with email claim)
Strapi MCP Plugin
↓ (resolves JWT email → Strapi admin user)
↓ (Document Service as that admin user)
Postgres
```
## FULL FLOW
### Step 1 — User Login
User authenticates via external IdP (Auth0, Cognito, Azure AD, etc).
Agent Orchestrator receives:
{
"sub": "user-123",
"email": "user@example.com",
"role": "Author",
"tenant": "org-456",
"permissions": ["article:create", "article:update"]
}
Orchestrator signs its own JWT for backend services.
### Step 2 — Tool Call
Agent calls Strapi MCP:
POST /mcp-server/messages
Authorization: Bearer <user-jwt>
X-MCP-Internal-Secret: <shared-secret>
**Two layers of security**:
- JWT → proves user identity
- Internal Secret → proves request came from trusted orchestrator
### Step3 - MCP Authentication
Inside your plugin:
1️⃣ Validate Internal Secret
Prevents random internet calls.
2️⃣ Validate JWT with IdP
Verify signature
Verify audience
Verify issuer
Check expiration
This confirms user identity.
### Step 4 — Authorization Layer (Critical)
Now we decide:
"Can THIS user perform THIS action on THIS content?"
This is NOT Strapi Admin RBAC.
This is your own policy layer.
Example internal service:
authorize({
user,
action: "create",
contentType: "api::article.article",
data
})
Policy enforces:
- Role permissions
- Field-level access
- Tenant scope
- Ownership rules
If denied → 403 immediately.
Content-Type Permission Map Example
const roleMatrix = {
Admin: ["*"],
Publisher: ["create", "read", "update", "publish"],
Author: ["create", "read", "update-own"],
Reader: ["read"]
};
### Step 5 — Execution as Resolved Admin User
If authorized, MCP executes as the resolved Strapi admin user:
```ts
// ctx.state.user = resolved admin user (from JWT email → admin lookup)
strapi.documents('api::article.article').create({ data: {...} })
// createdBy = the resolved admin user's ID (automatic)
```
The MCP client IS the Strapi admin user. No service account, no impersonation.
## Implementation Suggest
Phase 1 — Core Infrastructure
1. JWT Validation Service
Create:
src/services/auth.service.ts
Responsibilities:
Verify JWT against IdP JWKS endpoint
Cache keys
Extract normalized user object
Return:
{
id,
email,
role,
tenant,
permissions
}
2. Authorization Service
Create:
src/services/authorization.service.ts
Responsibilities:
Map role → allowed actions
Enforce content-type rules
Enforce field restrictions
Inject tenant filters
Inject ownership filters
3. Policy Execution Pipeline
Inside controller:
validateSecret()
→ validateJWT()
→ authorize()
→ sanitizeInput()
→ executeDocumentService()
→ sanitizeOutput()
→ streamResult()
### Setup Steps
| Step | Action |
|------|--------|
| 1 | Create Strapi admin users for each MCP client (matching email from IdP) |
| 2 | Set MCP_SECRET_KEY in .env (shared with orchestrator) |
| 3 | Configure JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URI in .env |
| 4 | Enable plugin in config/plugins.ts |
Middleware behavior:
- Validates X-MCP-Internal-Secret header
- Verifies JWT and extracts email claim
- Resolves Strapi admin user by email
- Sets ctx.state.user for Document Service calls
---
# Critical Problem: End-User Identity & CRUD
## Core Constraint
- IdP / SSO handled externally
- No end-user authentication inside Strapi
- MCP tools must enforce user-scoped permissions
---
# Recommended Secure Architecture
## DO NOT
- Use Admin RBAC for end users (Admin RBAC is for CMS operators)
- Expose Admin credentials
- Trust client-supplied roles
- Allow MCP to operate as unrestricted super admin
---
# Correct Security Model
## Architecture Overview
End User
↓
Agent Orchestrator Web App
↓ (JWT with user identity and role)
Strapi MCP Plugin
↓
Document Service
↓
Postgres
---
# Identity Propagation Model
### Step 1 – User Login
User authenticates with external IdP.
Orchestrator issues:
```json
{
"sub": "user-123",
"role": "Author",
"tenant": "org-456",
"scopes": ["article:create", "article:update"]
}
```
Signed JWT.
---
### Step 2 – Agent Tool Call
Agent calls:
POST /mcp-server/messages
Authorization: Bearer <user-jwt>
X-MCP-Internal-Secret: <server-secret>
---
### Step 3 – MCP Plugin Validation
Plugin must:
1. Validate JWT signature
2. Extract:
- userId
- role
- scopes
- tenant
3. Build execution context
---
# Access Control Enforcement Strategy
## Option A – Use Admin RBAC (Community Version Limitation)
Not recommended.
Admin RBAC:
- Designed for CMS operators
- Tied to admin panel authentication
- Not intended for runtime end-user context
- Hard to dynamically impersonate safely
---
## Option B – Resolved Admin User + Authorization Layer (Implemented)
MCP plugin resolves each client to a real Strapi admin user (by JWT email claim).
Before any Document Service call:
1. Resolve content type
2. Resolve action (create/read/update/delete/publish)
3. Enforce authorization policy based on:
- User role (from JWT or Strapi admin roles)
- field-level restrictions
- ownership (native createdBy)
- tenant scoping
---
# Proposed Authorization Layer
## 1. Role Matrix
| Role | Capabilities |
|------|-------------|
| Admin | Full access |
| Publisher | create/read/update/publish |
| Author | create/read/update own |
| Reader | read only |
---
## 2. Content Type Enforcement
Policy logic must validate:
```ts
canPerform(role, contentType, action)
```
---
## 3. Field-Level Enforcement
Before write operations:
- Strip forbidden fields
- Reject forbidden publish flag
- Validate relation access
---
## 4. Ownership Enforcement
Works natively because MCP client is resolved to a real Strapi admin user:
```ts
if (role === "Author") {
// user.id IS the Strapi admin user ID — createdBy matches naturally
query.filters.createdBy = user.id;
}
```
---
## 5. Multi-Tenant Enforcement (If Required)
Add implicit filter:
```ts
query.filters.tenantId = jwt.tenant
```
Never allow client to override tenant filter.
---
# Public vs Private Content Types
## Rule
MCP plugin ignores REST public/private model.
It must:
- Enforce custom authorization layer
- Never rely on REST API tokens
- Never expose admin endpoints
---
# Recommended Execution Pipeline (Need review/revision)
Incoming MCP Tool Call
↓
Validate JWT
↓
Resolve Content Type
↓
Resolve Action
↓
Authorization Policy Check
↓
Apply Implicit Filters (tenant, ownership)
↓
Sanitize Input Fields
↓
Execute Document Service Call
↓
Sanitize Output Fields
↓
Stream Response
---
# Security Considerations
## 1. Never Trust Client-Declared Role
Always verify JWT signature.
---
## 2. Never Use Super Admin for Tool Execution
Service user should:
- Have full internal access, but mimic end-user scope of access/activity
- But runtime access always restricted by policy layer
---
## 3. Prevent Privilege Escalation
Do not allow:
- Role override in request body
- Content type override beyond whitelist
- Raw query injection
---
## 4. Logging
Log:
- userId
- action
- contentType
- filters applied
- timestamp
For audit trail.
---
# When Would Admin RBAC Be Viable?
Only if:
- You use Strapi Enterprise SSO
- You create real Admin users per end-user
- You accept admin-table scale concerns
Otherwise:
Use service user + custom runtime policy layer.
---
# Development Checklist
## MCP Plugin Completion
- [x] Streamable HTTP controller (events.controller.ts)
- [x] MCP auth middleware (mcp-auth.ts) — resolves client to Strapi admin user
- [x] JWT validation service (auth.service.ts)
- [x] Authorization service with native createdBy ownership
- [x] Schema-aware tenant filter injection
- [x] Route wiring (POST/GET/DELETE /mcp)
- [x] Session cleanup on destroy
- [x] Plugin config schema
- [x] Port remaining tools (media, relations, publish/unpublish, delete/publish/unpublish single type, dev mode schema tools)
## Secure Plugins Completion
- [x] secure-documents plugin scaffold (package.json, tsconfig, entry point)
- [x] secure-search plugin scaffold (package.json, tsconfig, entry point)
- [x] Document content type schema
- [x] ABAC service (evaluator + SQL filter builder)
- [x] Storage service (S3/MinIO via AWS SDK v3)
- [x] Document controller (upload, list, get, update, delete, download, reindex, status)
- [x] Auth middleware (shared pattern from mcp-server)
- [x] Extractor service (PDF, DOCX, text)
- [x] Chunker service (sliding window, 800 tokens, 100 overlap)
- [x] Embedding service (Ollama + Bedrock providers)
- [x] Vector service (pgvector CRUD)
- [x] Pipeline service (extract → chunk → embed → store)
- [x] Bootstrap hook (CREATE EXTENSION vector, create tables)
- [x] Search service (embed query → pgvector → ABAC re-check)
- [x] Facet service (SQL GROUP BY with ABAC filter)
- [x] RAG service (prompt building + Ollama/Bedrock LLM)
- [x] Docker setup (PostgreSQL/pgvector + MinIO + Ollama)
- [x] Integration tests (09: CRUD, 10: ABAC, 11: search + RAG)
- [x] Production hardening (session limits, input validation, error sanitization, filename sanitization, query limits)
---
# Final Architecture Summary
The MCP Server:
- Is a Strapi 5 plugin
- Uses Document Service directly
- Uses Streamable HTTP transport (not SSE)
- Resolves each MCP client to a real Strapi admin user
- Authenticates via external JWT or Strapi admin token
- Enforces custom RBAC + native createdBy ownership
- Never uses a shared service account
- Never exposes Admin credentials
- Never depends on REST public/private settings
---
# Design Principle
Strapi = Data Engine
MCP Plugin = Secure Execution Layer
Agent Orchestrator = Identity Authority
Each system has a clear responsibility boundary.