@studioassistant/mcp-bridge
v0.1.7
Published
Studio Assistant MCP Bridge — connect AI agents to Studio Assistant infrastructure
Readme
MCP Server — Studio Assistant
Status: Planning / Phase 1 POC
Target: Q4 2026 — Q1 2027
Prerequisites: docs/AGENT_CONTEXT.md, docs/RULES.md
Purpose
A Model Context Protocol (MCP) server that exposes Studio Assistant's infrastructure as tools any MCP-compatible AI agent can use — Claude Desktop, Cursor, GitHub Copilot, and future AI-native interfaces.
This is the thin end of the wedge for the infrastructure layer thesis: the frontend becomes one interaction surface, the real value lives in the permissions, provenance, and structured metadata underneath.
Architecture
Two deployment modes:
Local Dev (Stdio Transport)
┌──────────────────────┐ MCP Protocol ┌──────────────────────┐
│ Claude Desktop │ ◄──────────────────► │ src/mcp-server/ │
│ Cursor │ stdio │ (standalone process)│
│ │ │ ./start.ts │
└──────────────────────┘ │ ./index.ts │
│ ./auth.ts │
│ ./tools/* │
└──────────┬───────────┘
│ reuses service fns
▼
┌──────────────────────┐
│ Existing Service │
│ Layer │
│ (models, helpers, │
│ prmChecker) │
└──────────────────────┘Production (Express Middleware)
┌──────────────────────┐ HTTPS + SSE ┌──────────────────────────┐
│ Claude Desktop │ ◄──────────────────► │ Existing Express App │
│ Cursor │ (remote) │ │
│ Custom Clients │ │ POST /api/mcp │
│ │ │ (mcp-server/express.ts) │
└──────────────────────┘ │ │
│ Uses existing auth MW │
│ (authenticateUser + │
│ prmChecker) │
│ │
│ Calls existing │
│ controllers/services │
└──────────────────────────┘In production, the MCP server mounts as Express middleware on the existing app — zero new infrastructure. It reuses:
authenticateUser()middleware for authprmChecker.tsfor permission gating- Existing service functions (
bookingService,orgService, etc.) for all business logic - Existing ingress, scaling, and monitoring
The standalone src/mcp-server/ directory is used for local development (stdio transport). Production mounts the same tool handlers via Express.
Transport comparison
| Aspect | Stdio (local dev) | SSE/HTTP (production) |
|--------|-------------------|----------------------|
| Connection | Child process stdin/stdout | HTTP + Server-Sent Events |
| Auth | Email code via MCP tools | OAuth via browser redirect |
| Claude Desktop config | command + args + env | url + Bearer token |
| SDK class | StdioServerTransport | SSEServerTransport |
| Debugging | console.error in logs | curl + browser devtools |
| Scaling | One process per client | One server, many clients |
Key Design Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Local dev | Standalone process (stdio) | Claude Desktop connects directly, no server needed |
| Production | Express middleware (SSE) | Reuses existing ingress, auth, scaling — no new infra |
| SDK | @modelcontextprotocol/sdk | Official Anthropic SDK, supports both stdio and Express transports |
| Auth (stdio) | Email code + sa_api_token | Interactive login via MCP tools. Tokens cached to disk for restart. |
| Auth (SSE) | OAuth via browser redirect | MCP spec compliant. Reuses existing Studio Assistant login page. |
| PRM | Reuse prmChecker.ts + setBaseUserPermissions() | Same permission logic as web app, zero duplication |
| Business logic | Call existing service functions | Tool handlers are thin wrappers around existing models/sa/* services |
Authentication Flow
Three auth modes (tried in order)
- Environment variable
SA_MCP_TOKEN— for CI/scripts/power users. Bypasses interactive auth. - Cached token file (
~/.config/studioassistant/mcp/token) — auto-created on first login, read on restart. Makes re-auth invisible. - Email code flow — interactive login via MCP tools when no token is available.
Email Code Flow (Interactive)
User: "Connect to Studio Assistant"
→ Claude calls auth_send_code({ email })
→ MCP server calls getUserByEmail(email)
→ Generates 6-digit code (sixDigitCode from existing utils)
→ Stores code in Redis (same as web auth flow)
→ Sends email via sendEmail() (same as web)
→ Returns "Code sent to email"
→ User: "The code is 482916"
→ Claude calls auth_verify_code({ email, code })
→ Validates code from Redis
→ Creates new sa_api_token row (auto-generated)
→ Writes token to ~/.config/studioassistant/mcp/token
→ Stores McpSession in memory
→ Returns user info + org list
From here: all tools work with the authenticated session.OAuth Flow (SSE/HTTP Transport)
For production deployments where the MCP server runs as an Express endpoint, the standard MCP OAuth flow is used:
Step 1: Client discovers auth metadata
GET /.well-known/oauth-authorization-server
← { authorization_endpoint, token_endpoint }
Step 2: Client opens browser to /oauth/authorize
→ User logs in via existing Studio Assistant login page
→ User grants MCP access consent
→ Auth code returned via redirect
Step 3: MCP server exchanges auth code for token
POST /oauth/token { grant_type: "authorization_code", code, ... }
← { access_token, token_type: "bearer" }
Step 4: Client calls tools with Bearer token
POST /message Authorization: Bearer <token>
→ authenticateToolCall() validates token against sa_api_token
→ Tool handler executes with authenticated sessionThis follows the MCP spec's authorization model exactly. The existing auth infrastructure is reused:
- Login page: Same
/loginEJS template - User sessions: Existing Redis session management
- Token storage: Same
sa_api_tokentable - Token validation: Same
authenticateToolCall()function
Token Persistence
The cached token file enables silent re-auth on restart:
Server starts → checks SA_MCP_TOKEN env var
→ if set: use it (CI/script mode)
→ if not: checks ~/.config/studioassistant/mcp/token
→ if file exists: validate token against sa_api_token
→ if valid: auto-auth, no login needed
→ if invalid/expired: require email login
→ if no file: require email loginDev Mode (localhost)
On localhost in NODE_ENV=development, the code 000000 is auto-accepted (same as web app). No email required. Rate limits are disabled for dev.
sa_api_token Table
| Column | Type | Purpose |
|--------|------|---------|
| id | INT UNSIGNED PK | Row ID |
| user_id | INT UNSIGNED FK → sa_user | Token owner |
| token | VARCHAR(255) UNIQUE | The actual token |
| label | VARCHAR(100) | User-friendly name ("Claude Desktop") |
| scopes | JSON | Optional future scoping |
| last_used_at | DATETIME | Audit tracking |
| expires_at | DATETIME | Optional expiry |
| created_at | DATETIME | Creation timestamp |
| revoked | TINYINT(1) | Manual revocation |
Setup for Developers
Quick start (one command)
npm run mcp:setupThis script:
- Reads your
.envfile for DB/Redis config - Detects your project path
- Creates a token for your user (or prompts for email)
- Writes the Claude Desktop config
- Prints any remaining instructions
Manual config (if you prefer)
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"studio-assistant": {
"command": "npx",
"args": ["tsx", "/path/to/studioassistant/src/mcp-server/start.ts"],
"env": {
"DATABASE_URL": "mysql://user:pass@host:3306/db",
"NODE_ENV": "development",
"REDIS_DB_HOST": "localhost",
"REDIS_DB_PORT": "6379",
"REDIS_DB_SESSION_HOST": "localhost",
"REDIS_DB_SESSION_PORT": "6380",
"DB_USER": "dev",
"DB_PASS": "password",
"DB_HOST": "127.0.0.1",
"DB_NAME": "dev"
}
}
}
}Without SA_MCP_TOKEN set, the server starts in interactive auth mode — send auth_send_code / auth_verify_code to log in.
PRM → Tool Access Mapping
| User Type | usertype | PRM at org | Can access | Availability response |
|-----------|-----------|------------|-----------|---------------------|
| No membership | '' | none (null) | find_studio, check_availability, request_booking, create_account | Abstract slots only |
| Session participant | '' | 0 (guest) | Above + create_booking, list_my_sessions | + room names + rates |
| Staff / Manager | '' | 1 | Above + list_studio_bookings, get_summary_feed, approve_booking, send_quote | Full calendar data |
| Owner | '' | 2 | Everything | Full |
| Global admin | 'g' | bypass | Everything | Full |
| Read-only | 'r' | depends | GET-only equivalents | Tiered |
| Check-in kiosk | 's' | studio-scoped | Check-in tools only | N/A |
Every tool checks PRM internally and returns INSUFFICIENT_PERMISSIONS if the user's token doesn't have sufficient level.
Tool Design Principles — Business Boundaries
The MCP server is not a database API. Every tool must enforce the same business rules the web app does. Raw data access is never exposed.
The rule
WRONG: "Get me all records from sa_session"
→ Exposes raw table. No business logic. Dangerous.
RIGHT: "Get sessions for next week"
→ Enforces time window. Filters by user's PRM. Returns only what the user can see.
RIGHT: "Create a booking"
→ Goes through storeBooking(). Enforces status lifecycle. Creates audit logs.
→ Same as web app's "Book" button.Enforcement layers
| Layer | What it guards | Example |
|-------|---------------|---------|
| Tool scope | What the tool can do | find_studio returns public info only. Cannot access bookings. |
| Input validation | What parameters are accepted | request_booking requires studioId, date, name, email. No raw SQL. |
| Service functions | Business logic + data integrity | storeBooking() sets defaults, validates status transitions, caches. |
| PRM middleware | Who can do what | list_studio_bookings requires PRM ≥ 1. Same gate as web. |
| Response shaping | What data is returned | Non-members see abstract slots only. Never internal IDs, never other users' data. |
What a tool boundary looks like
// Tool: request_booking
// INPUT: { studioId, date, name, email, phone?, notes? }
// OUTPUT: { bookingId, status: "setup", message }
// CALLS: getOrgById() → validates studio exists
// storeBooking() → enforces booking lifecycle
// (returns success — studio reviews later)
// CANNOT: Access other users' bookings
// CANNOT: Skip the setup → approval flow
// CANNOT: Modify existing bookings
// CANNOT: Access pricing or financial dataWhat we do NOT expose as tools
| Capability | Why excluded | |------------|-------------| | Raw Prisma queries | By-passes all business logic, caching, and audit trails | | Direct table access | No PRM enforcement, no data shaping | | User impersonation | Only the token holder's session is used | | Bulk data exports | No rate limiting, no pagination boundaries | | Schema mutations | Database structure changes need migrations, not MCP | | Cross-org data access | PRM enforcement per tool — no "admin" bypass | | Session modification | Tokens are created via auth flow, never manipulated directly |
Comparison with web controllers
// Web controller — same boundaries
export const getBooking = async (req, res) => {
authenticateUser(req); // Who are you?
prmCheckBooking(req); // Can you see this?
validateRequest(schema); // Is the input valid?
const data = getBookingById(); // Business logic
processBookingDetail(data); // Shape response
res.json({ data }); // Return only what's permitted
};
// MCP tool — identical boundaries
async function handleGetBooking(args, session) {
authenticateToolCall(token); // Who are you?
prmCheckBooking(session, args.id); // Can you see this?
validateInput(schema, args); // Is the input valid?
const data = await getBookingById();// Business logic (same service)
const shaped = shapeForOutput(data, session); // Shape response
return { content: [shaped] }; // Return only what's permitted
}The same service functions. The same PRM gates. The same response shaping. Different transport.
Architecture Note: MCP vs Web UI Privacy
Web UI: A rendered calendar visually leaks information — room names, session durations, booking titles, operational patterns. Even without explicit labels, the layout reveals studio activity. This is why the web app restricts availability to members only.
MCP Engine: Returns structured data — [{ start, end, status: "available" }]. No names, no booking titles, no patterns. The AI agent doesn't render a calendar; it communicates abstract time windows. The same query that returns raw availability data is fundamentally different from displaying a visual calendar.
The tiered response model
| Access level | Availability response | Booking flow |
|-------------|----------------------|--------------|
| No membership | Available time slots only. No room names, no rates, no booking IDs. | request_booking → status: setup → studio reviews → quote |
| Member (PRM ≥ 0) | Room names + rates included. Full slot details. | create_booking → status: setup (or booked if autobook enabled) |
| Staff (PRM ≥ 1) | Full calendar data. | Direct status advancement, quotes, approvals |
Auto-book
If a user has a qualifying membership or share that grants auto-book permissions, create_booking can advance directly to status: booked (status 2) instead of status: setup (status 0). The same storeBooking() service handles both cases — it's a parameter change.
Tool Inventory
Auth & Account
| Tool | PRM Required | Description |
|------|-------------|-------------|
| auth_send_code | none | Send 6-digit verification code to email (or 000000 on localhost dev) |
| auth_verify_code | none | Verify code, authenticate session, return token |
| create_account | none | Create new user account (email + verification) |
Studio Directory
| Tool | PRM Required | Description |
|------|-------------|-------------|
| find_studio | none | Search studios by name |
| list_all_studios | none | Public studio directory |
Dashboard
| Tool | PRM Required | Description |
|------|-------------|-------------|
| list_my_orgs | authenticated | User's orgs + artists + PRM levels |
| get_summary_feed | PRM ≥ 1 | Org notification feed |
Bookings
| Tool | PRM Required | Description |
|------|-------------|-------------|
| request_booking | authenticated | Submit booking inquiry |
| list_my_bookings | authenticated | User's bookings as contact |
| list_studio_bookings | PRM ≥ 1 | Staff booking list |
Future
| Tool | PRM Required | Description | Response differs by |
|------|-------------|-------------|-------------------|
| check_availability | authenticated (any user) | Check available time slots | membership tier |
| create_booking | authenticated | Create a booking (setup or booked) | membership tier |
| approve_booking | PRM ≥ 1 | Advance booking status | — |
| send_quote | PRM ≥ 1 | Generate and send quote | — |
Directory Structure
docs/plans/MCP/
README.md ← This file
tools/
tool-specs.md ← Full schemas for every tool
data-models.md ← Shared data shapes
scenes/
scene-1-customer-inquiry.md ← Non-member → request booking
scene-2-member-booking.md ← Active share → book with pricing
scene-3-staff-approval.md ← Manager → pending list → approve
implementation/
phase-1-poc.md ← Build plan for POC
db-tables.sql ← Raw SQL for sa_api_tokenFuture Vision
The end state: users interact with Studio Assistant entirely through AI agents.
Artist: "I want to book Ocean Way for next Tuesday"
→ Agent checks identity (auto-auth from cache)
→ Searches studios
→ Checks availability
→ Creates booking
→ Sets up session with participants
→ Invites collaborators
→ Tracks contributions during session
→ Generates reports for labels/publishersNo web UI needed. All of this is achievable by wrapping existing service functions as MCP tools. The infrastructure layer is the product — the frontend is just one interface.
Full MCP-native user journey
- Create account (
create_account) - Log in (
auth_send_code→auth_verify_code) - Browse studios (
find_studio,list_all_studios) - Book sessions (
request_booking,create_booking) - Track projects (
list_my_bookings,get_summary_feed) - Verify contributions (future:
get_session,list_participants) - Manage royalties (future:
get_contributions,verify_attendance)
Demo Video Outline
The POC output is a 3-5 minute demo video showing:
- Login scene: "Connect to Studio Assistant" → email code → auto-authenticated
- Customer scene: "Book Ocean Way for next Tuesday" → agent finds studio, creates inquiry
- Staff scene: "What needs approval?" → agent lists pending, reviews, approves
- Provenance query (stretch): "Who played guitar on track X?" → agent traces through sessions, participants, roles
Each scene shows the identity model: the same MCP server, same tools, different results based on who's asking.
