Endpoints Your tenant app must implement 5 API endpoints. The hub calls these endpoints server-to-server (except the login page, which the user's browser visits). Two additional optional endpoints (GET /api/mcp/users and GET /api/mcp/roles) can be implemented to enable LockMCP to auto-populate the member and role directories.
Endpoint HMAC signed? Purpose POST /api/mcp/loginNo Form POST from login page — issues auth code POST /api/mcp/exchangeYes Exchange auth code + PKCE verifier for session token POST /api/mcp/validateYes Validate a session token, return TTL + roles POST /api/mcp/refreshYes Rotate session token atomically POST /api/mcp/logoutYes Revoke a session token GET /api/mcp/usersTENANT_DELEGATED only Optional. Return list of tenant usersGET /api/mcp/rolesTENANT_DELEGATED only Optional. Return list of tenant roles
POST /api/mcp/login Handles the form POST from the login page. Authenticates the user, issues a short-lived auth code, and redirects back to the hub's callback URL with the code.
HMAC: Not required (this is a browser form submission, not a server-to-server call).
Request: application/x-www-form-urlencoded (form data)
Field Description emailUser's email address passwordUser's password callback_urlHub callback URL (must be in allowlist) stateOpaque state string — pass back unchanged code_challengePKCE S256 challenge (43-char base64url)
Success: 302 redirect to {callback_url}?state={state}&code={code}
Failure: 302 redirect to {callback_url}?state={state}&error=access_denied
Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/mcp/login/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { issueAuthCode } from "@/lib/mcp-contract/auth-code-store";
import { isAllowedCallbackUrl } from "@/lib/mcp-contract/config";
// PKCE S256 challenge is always 43 chars of base64url
const CODE_CHALLENGE_RE = /^[A-Za-z0-9_-]{43}$/;
export async function POST(request: NextRequest) {
const formData = await request.formData();
const email = formData.get("email") as string | null;
const password = formData.get("password") as string | null;
const callbackUrl = formData.get("callback_url") as string | null;
const state = formData.get("state") as string | null;
const codeChallenge = formData.get("code_challenge") as string | null;
if (!email || !password || !callbackUrl || !state || !codeChallenge) {
return NextResponse.json(
{ error: "Missing required fields" }, { status: 400 });
}
// Open-redirect defense
if (!isAllowedCallbackUrl(callbackUrl)) {
return NextResponse.json(
{ error: "callback_url host not allowed" }, { status: 400 });
}
// PKCE S256 format check
if (!CODE_CHALLENGE_RE.test(codeChallenge)) {
return NextResponse.json(
{ error: "Invalid code_challenge: must be 43-char base64url (S256)" },
{ status: 400 });
}
let userId: string;
try {
const result = await auth.api.signInEmail({
body: { email, password },
});
if (!result?.user) throw new Error("Auth returned no user");
userId = result.user.id;
} catch {
// Auth failed — redirect with error
const errorUrl = new URL(callbackUrl);
errorUrl.searchParams.set("state", state);
errorUrl.searchParams.set("error", "access_denied");
return NextResponse.redirect(errorUrl.toString(), { status: 302 });
}
// Issue auth code and redirect back to hub
const code = issueAuthCode(userId, codeChallenge);
const successUrl = new URL(callbackUrl);
successUrl.searchParams.set("state", state);
successUrl.searchParams.set("code", code);
return NextResponse.redirect(successUrl.toString(), { status: 302 });
}// app/api/mcp/login/route.js
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { issueAuthCode } from "@/lib/mcp-contract/auth-code-store";
import { isAllowedCallbackUrl } from "@/lib/mcp-contract/config";
const CODE_CHALLENGE_RE = /^[A-Za-z0-9_-]{43}$/;
export async function POST(request) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const callbackUrl = formData.get("callback_url");
const state = formData.get("state");
const codeChallenge = formData.get("code_challenge");
if (!email || !password || !callbackUrl || !state || !codeChallenge) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
if (!isAllowedCallbackUrl(callbackUrl)) {
return NextResponse.json(
{ error: "callback_url host not allowed" }, { status: 400 });
}
if (!CODE_CHALLENGE_RE.test(codeChallenge)) {
return NextResponse.json(
{ error: "Invalid code_challenge: must be 43-char base64url (S256)" },
{ status: 400 });
}
let userId;
try {
const result = await auth.api.signInEmail({ body: { email, password } });
if (!result?.user) throw new Error("Auth returned no user");
userId = result.user.id;
} catch {
const errorUrl = new URL(callbackUrl);
errorUrl.searchParams.set("state", state);
errorUrl.searchParams.set("error", "access_denied");
return NextResponse.redirect(errorUrl.toString(), { status: 302 });
}
const code = issueAuthCode(userId, codeChallenge);
const successUrl = new URL(callbackUrl);
successUrl.searchParams.set("state", state);
successUrl.searchParams.set("code", code);
return NextResponse.redirect(successUrl.toString(), { status: 302 });
}// McpLoginController.java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
@RestController
public class McpLoginController {
private static final Pattern CODE_CHALLENGE_RE =
Pattern.compile("^[A-Za-z0-9_-]{43}$");
private final McpContractProperties props;
private final AuthService authService;
private final AuthCodeStore authCodeStore;
public McpLoginController(McpContractProperties props,
AuthService authService,
AuthCodeStore authCodeStore) {
this.props = props;
this.authService = authService;
this.authCodeStore = authCodeStore;
}
@PostMapping("/api/mcp/login")
public RedirectView login(
@RequestParam("email") String email,
@RequestParam("password") String password,
@RequestParam("callback_url") String callbackUrl,
@RequestParam("state") String state,
@RequestParam("code_challenge") String codeChallenge) {
// Open-redirect defense
if (!props.isAllowedCallbackUrl(callbackUrl)) {
return errorRedirect(callbackUrl, state, "access_denied");
}
// PKCE S256 format check
if (!CODE_CHALLENGE_RE.matcher(codeChallenge).matches()) {
return errorRedirect(callbackUrl, state, "access_denied");
}
// Authenticate user against your user directory
String userId;
try {
userId = authService.authenticate(email, password);
} catch (Exception e) {
return errorRedirect(callbackUrl, state, "access_denied");
}
// Issue auth code and redirect back to hub
String code = authCodeStore.issue(userId, codeChallenge);
var redirect = new RedirectView(
callbackUrl + "?state=" + enc(state) + "&code=" + enc(code));
redirect.setStatusCode(HttpStatus.FOUND);
return redirect;
}
private RedirectView errorRedirect(String callbackUrl,
String state, String error) {
var redirect = new RedirectView(
callbackUrl + "?state=" + enc(state) +
"&error=" + enc(error));
redirect.setStatusCode(HttpStatus.FOUND);
return redirect;
}
private static String enc(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}
}// routes/mcp-login.ts
import { Router } from "express";
import { issueAuthCode } from "../lib/mcp-contract/auth-code-store";
import { isAllowedCallbackUrl } from "../lib/mcp-contract/config";
import { authenticateUser } from "../lib/auth"; // replace with your auth logic
const router = Router();
const CODE_CHALLENGE_RE = /^[A-Za-z0-9_-]{43}$/;
router.post("/api/mcp/login",
require("express").urlencoded({ extended: false }),
async (req, res) => {
const { email, password, callback_url, state, code_challenge } = req.body as {
email: string; password: string; callback_url: string;
state: string; code_challenge: string;
};
if (!email || !password || !callback_url || !state || !code_challenge) {
return res.status(400).json({ error: "Missing required fields" });
}
if (!isAllowedCallbackUrl(callback_url)) {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
if (!CODE_CHALLENGE_RE.test(code_challenge)) {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
try {
const userId = await authenticateUser(email, password);
const code = issueAuthCode(userId, code_challenge);
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&code=${encodeURIComponent(code)}`);
} catch {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
}
);
export default router;// routes/mcp-login.js
const { Router } = require("express");
const { issueAuthCode } = require("../lib/mcp-contract/auth-code-store");
const { isAllowedCallbackUrl } = require("../lib/mcp-contract/config");
const { authenticateUser } = require("../lib/auth"); // replace with your auth logic
const router = Router();
const CODE_CHALLENGE_RE = /^[A-Za-z0-9_-]{43}$/;
router.post("/api/mcp/login",
require("express").urlencoded({ extended: false }),
async (req, res) => {
const { email, password, callback_url, state, code_challenge } = req.body;
if (!email || !password || !callback_url || !state || !code_challenge) {
return res.status(400).json({ error: "Missing required fields" });
}
// Open-redirect defense
if (!isAllowedCallbackUrl(callback_url)) {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
// PKCE S256 format check
if (!CODE_CHALLENGE_RE.test(code_challenge)) {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
try {
const userId = await authenticateUser(email, password);
const code = issueAuthCode(userId, code_challenge);
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&code=${encodeURIComponent(code)}`);
} catch {
return res.redirect(302,
`${callback_url}?state=${encodeURIComponent(state)}&error=access_denied`);
}
}
);
module.exports = router;# routers/mcp_login.py
import re
from fastapi import APIRouter, Form
from fastapi.responses import RedirectResponse
from mcp_contract.config import is_allowed_callback_url
from mcp_contract.auth_code_store import issue_auth_code
from your_auth import authenticate_user # replace with your auth logic
router = APIRouter()
CODE_CHALLENGE_RE = re.compile(r"^[A-Za-z0-9_-]{43}$")
@router.post("/api/mcp/login")
async def mcp_login(
email: str = Form(...),
password: str = Form(...),
callback_url: str = Form(...),
state: str = Form(...),
code_challenge: str = Form(...),
):
# Open-redirect defense
if not is_allowed_callback_url(callback_url):
return RedirectResponse(
f"{callback_url}?state={state}&error=access_denied",
status_code=302,
)
# PKCE S256 format check
if not CODE_CHALLENGE_RE.match(code_challenge):
return RedirectResponse(
f"{callback_url}?state={state}&error=access_denied",
status_code=302,
)
try:
user_id = authenticate_user(email, password)
except Exception:
return RedirectResponse(
f"{callback_url}?state={state}&error=access_denied",
status_code=302,
)
# Issue auth code and redirect back to hub
code = issue_auth_code(user_id, code_challenge)
return RedirectResponse(
f"{callback_url}?state={state}&code={code}", status_code=302
)POST /api/mcp/exchange Exchanges an auth code (from the login flow) and a PKCE code verifier for a session token.
Request headers: HMAC signature headers (X-Hub-Timestamp, X-Hub-Nonce, X-Hub-Signature-256)
Request body:
{
"code": "base64url-encoded-auth-code",
"code_verifier": "original-PKCE-verifier-string"
}Success response (200):
{
"user_id": "user-uuid",
"email": "user@example.com",
"session_token": "mcp:a1b2c3d4...",
"expires_in": 3600,
"roles": ["admin"],
"display_name": "Jane Doe",
"enum_scopes": {
"email": "user@example.com",
"accounts": [
{
"accountId": 456,
"roles": [
{ "role": "LEASING_AGENT", "propertyIds": [101, 102] }
]
}
]
}
}enum_scopes is optional — include it to enable enum injection . The hub stores this payload on the session record and uses it to inject enum constraints into tool parameters marked with x-mcp-inject. The key names inside each role entry (e.g. propertyIds) must match the x-mcp-inject values in your OpenAPI spec. Omit enum_scopes entirely if your tenant doesn't use enum injection.
getUserEnumScopes (shown in the examples below) is a placeholder for your own data layer — implement it to query your database for the user's accounts and roles. See Enum Injection for the expected payload shape.
Error responses:
401 — invalid/expired code, PKCE mismatch, HMAC failure400 — missing code or code_verifierNext.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/mcp/exchange/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { consumeAuthCode } from "@/lib/mcp-contract/auth-code-store";
import { createMcpSession } from "@/lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "@/lib/mcp-contract/role-mapping";
import { getUserEnumScopes } from "@/lib/mcp-contract/enum-scopes"; // optional — your own implementation
import { prisma } from "@/lib/prisma";
const SESSION_TTL_SEC = parseInt(
process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10);
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body: { code?: string; code_verifier?: string };
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json(
{ error: "Invalid JSON" }, { status: 400 }); }
const { code, code_verifier } = body;
if (!code || !code_verifier) {
return NextResponse.json(
{ error: "Missing code or code_verifier" }, { status: 400 });
}
const record = consumeAuthCode(code, code_verifier);
if (!record) {
return NextResponse.json(
{ error: "invalid_code" }, { status: 401 });
}
const user = await prisma.user.findUnique({
where: { id: record.userId },
});
if (!user) {
return NextResponse.json(
{ error: "user_not_found" }, { status: 401 });
}
const [{ token, expiresIn }, roles, enumScopes] = await Promise.all([
createMcpSession(user.id, SESSION_TTL_SEC),
getUserRolesForMcp(user.id),
getUserEnumScopes(user.id), // returns null if not using enum injection
]);
return NextResponse.json({
user_id: user.id,
email: user.email,
session_token: token,
expires_in: expiresIn,
roles,
display_name: user.name,
...(enumScopes && { enum_scopes: enumScopes }),
});
}// app/api/mcp/exchange/route.js
import { NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { consumeAuthCode } from "@/lib/mcp-contract/auth-code-store";
import { createMcpSession } from "@/lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "@/lib/mcp-contract/role-mapping";
import { getUserEnumScopes } from "@/lib/mcp-contract/enum-scopes"; // optional — your own implementation
import { prisma } from "@/lib/prisma";
import { MCP_CONTRACT_SESSION_TTL_SEC } from "@/lib/mcp-contract/config";
export async function POST(request) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body;
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); }
const { code, code_verifier } = body;
if (!code || !code_verifier) {
return NextResponse.json(
{ error: "Missing code or code_verifier" }, { status: 400 });
}
const record = consumeAuthCode(code, code_verifier);
if (!record) {
return NextResponse.json({ error: "invalid_code" }, { status: 401 });
}
const user = await prisma.user.findUnique({ where: { id: record.userId } });
if (!user) {
return NextResponse.json({ error: "user_not_found" }, { status: 401 });
}
const [{ token, expiresIn }, roles, enumScopes] = await Promise.all([
createMcpSession(user.id, MCP_CONTRACT_SESSION_TTL_SEC),
getUserRolesForMcp(user.id),
getUserEnumScopes(user.id),
]);
return NextResponse.json({
user_id: user.id,
email: user.email,
session_token: token,
expires_in: expiresIn,
roles,
display_name: user.name,
...(enumScopes && { enum_scopes: enumScopes }),
});
}// McpExchangeController.java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class McpExchangeController {
private final AuthCodeStore authCodeStore;
private final SessionTokenService sessionService;
private final UserRepository userRepo;
private final McpContractProperties props;
private final EnumScopesService enumScopesService; // optional — your own implementation; remove if not using enum injection
public McpExchangeController(AuthCodeStore authCodeStore,
SessionTokenService sessionService,
UserRepository userRepo,
McpContractProperties props,
EnumScopesService enumScopesService) {
this.authCodeStore = authCodeStore;
this.sessionService = sessionService;
this.userRepo = userRepo;
this.props = props;
this.enumScopesService = enumScopesService;
}
// HMAC verification is handled by HmacVerificationFilter
@PostMapping("/api/mcp/exchange")
public ResponseEntity<?> exchange(
@RequestBody ExchangeRequest req) {
if (req.code() == null || req.code_verifier() == null) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Missing code or code_verifier"));
}
// Consume auth code + verify PKCE
String userId = authCodeStore.consume(
req.code(), req.code_verifier());
if (userId == null) {
return ResponseEntity.status(401)
.body(Map.of("error", "invalid_code"));
}
var user = userRepo.findById(userId).orElse(null);
if (user == null) {
return ResponseEntity.status(401)
.body(Map.of("error", "user_not_found"));
}
var token = sessionService.createSession(
userId, props.sessionTtlSec());
// optional — omit enumScopesService entirely if not using enum injection
var enumScopes = enumScopesService.getUserEnumScopes(userId);
var response = new java.util.LinkedHashMap<String, Object>();
response.put("user_id", user.getId());
response.put("email", user.getEmail());
response.put("session_token", token.token());
response.put("expires_in", token.expiresIn());
response.put("roles", user.getRoles());
response.put("display_name", user.getDisplayName());
if (enumScopes != null) response.put("enum_scopes", enumScopes);
return ResponseEntity.ok(response);
}
record ExchangeRequest(String code, String code_verifier) {}
}// routes/mcp-exchange.ts
import { Router } from "express";
import { hmacMiddleware } from "../lib/mcp-contract/hmac";
import { consumeAuthCode } from "../lib/mcp-contract/auth-code-store";
import { createMcpSession } from "../lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "../lib/mcp-contract/role-mapping";
import { getUserEnumScopes } from "../lib/mcp-contract/enum-scopes"; // optional — your own implementation
import { prisma } from "../lib/prisma";
import { MCP_CONTRACT_SESSION_TTL_SEC } from "../lib/mcp-contract/config";
const router = Router();
router.post("/api/mcp/exchange", hmacMiddleware, async (req, res) => {
const { code, code_verifier } = req.body as {
code: string; code_verifier: string;
};
if (!code || !code_verifier) {
return res.status(400).json({ error: "Missing code or code_verifier" });
}
const record = consumeAuthCode(code, code_verifier);
if (!record) return res.status(401).json({ error: "invalid_code" });
const user = await prisma.user.findUnique({ where: { id: record.userId } });
if (!user) return res.status(401).json({ error: "user_not_found" });
const [{ token, expiresIn }, roles, enumScopes] = await Promise.all([
createMcpSession(user.id, MCP_CONTRACT_SESSION_TTL_SEC),
getUserRolesForMcp(user.id),
getUserEnumScopes(user.id),
]);
return res.json({
user_id: user.id,
email: user.email,
session_token: token,
expires_in: expiresIn,
roles,
display_name: user.name,
...(enumScopes && { enum_scopes: enumScopes }),
});
});
export default router;// routes/mcp-exchange.js
const { Router } = require("express");
const { hmacMiddleware } = require("../lib/mcp-contract/hmac");
const { consumeAuthCode } = require("../lib/mcp-contract/auth-code-store");
const { createMcpSession } = require("../lib/mcp-contract/session-token");
const { getUserRolesForMcp } = require("../lib/mcp-contract/role-mapping");
const { getUserEnumScopes } = require("../lib/mcp-contract/enum-scopes"); // optional — your own implementation
const { prisma } = require("../lib/prisma");
const { MCP_CONTRACT_SESSION_TTL_SEC } = require("../lib/mcp-contract/config");
const router = Router();
router.post("/api/mcp/exchange", hmacMiddleware, async (req, res) => {
const { code, code_verifier } = req.body;
if (!code || !code_verifier) {
return res.status(400).json({ error: "Missing code or code_verifier" });
}
const record = consumeAuthCode(code, code_verifier);
if (!record) return res.status(401).json({ error: "invalid_code" });
const user = await prisma.user.findUnique({ where: { id: record.userId } });
if (!user) return res.status(401).json({ error: "user_not_found" });
const [{ token, expiresIn }, roles, enumScopes] = await Promise.all([
createMcpSession(user.id, MCP_CONTRACT_SESSION_TTL_SEC),
getUserRolesForMcp(user.id),
getUserEnumScopes(user.id),
]);
return res.json({
user_id: user.id,
email: user.email,
session_token: token,
expires_in: expiresIn,
roles,
display_name: user.name,
...(enumScopes && { enum_scopes: enumScopes }),
});
});
module.exports = router;# routers/mcp_exchange.py
import os
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from mcp_contract.hmac_verify import verify_hub_signature
from mcp_contract.auth_code_store import consume_auth_code
from mcp_contract.session_token import create_mcp_session
from your_db import get_db # replace with your DB session factory
from your_users import get_user, get_user_roles # replace with your lookups
# Optional — only needed for enum injection:
from your_roles import get_user_enum_scopes
router = APIRouter()
SESSION_TTL_SEC = int(os.environ.get("MCP_CONTRACT_SESSION_TTL_SEC", "3600"))
class ExchangeRequest(BaseModel):
code: str
code_verifier: str
@router.post("/api/mcp/exchange", dependencies=[Depends(verify_hub_signature)])
async def mcp_exchange(body: ExchangeRequest, db=Depends(get_db)):
record = consume_auth_code(body.code, body.code_verifier)
if not record:
raise HTTPException(401, detail="invalid_code")
user = get_user(db, record["user_id"])
if not user:
raise HTTPException(401, detail="user_not_found")
session = create_mcp_session(db, user.id, SESSION_TTL_SEC)
roles = get_user_roles(db, user.id)
enum_scopes = get_user_enum_scopes(db, user.id) # optional — returns None if not using enum injection
response = {
"user_id": user.id,
"email": user.email,
"session_token": session["token"],
"expires_in": session["expires_in"],
"roles": roles,
"display_name": user.display_name,
}
if enum_scopes:
response["enum_scopes"] = enum_scopes
return responsePOST /api/mcp/validate Validates a session token and returns whether it's valid, the remaining TTL, and the user's roles.
Request body:
{ "session_token": "mcp:a1b2c3d4..." }Response (200) — valid token:
{ "valid": true, "remaining_seconds": 3542, "roles": ["admin"] }Response (200) — expired/invalid token:
{ "valid": false, "remaining_seconds": 0 }Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/mcp/validate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { findMcpSession } from "@/lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "@/lib/mcp-contract/role-mapping";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body: { session_token?: string };
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json(
{ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json(
{ error: "Missing session_token" }, { status: 400 });
}
const session = await findMcpSession(session_token);
if (!session) {
return NextResponse.json(
{ valid: false, remaining_seconds: 0 });
}
// Verify user still exists
const user = await prisma.user.findUnique({
where: { id: session.userId },
select: { id: true },
});
if (!user) {
return NextResponse.json(
{ valid: false, remaining_seconds: 0 });
}
const remainingMs = session.expiresAt.getTime() - Date.now();
const remainingSeconds = Math.floor(remainingMs / 1000);
const roles = await getUserRolesForMcp(session.userId);
return NextResponse.json({
valid: true, remaining_seconds: remainingSeconds, roles,
});
}// app/api/mcp/validate/route.js
import { NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { findMcpSession } from "@/lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "@/lib/mcp-contract/role-mapping";
import { prisma } from "@/lib/prisma";
export async function POST(request) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body;
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json({ error: "Missing session_token" }, { status: 400 });
}
const session = await findMcpSession(session_token);
if (!session) {
return NextResponse.json({ valid: false, remaining_seconds: 0 });
}
const user = await prisma.user.findUnique({
where: { id: session.userId },
select: { id: true },
});
if (!user) {
return NextResponse.json({ valid: false, remaining_seconds: 0 });
}
const remainingMs = session.expiresAt.getTime() - Date.now();
const remainingSeconds = Math.floor(remainingMs / 1000);
const roles = await getUserRolesForMcp(session.userId);
return NextResponse.json({
valid: true, remaining_seconds: remainingSeconds, roles,
});
}// McpValidateController.java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@RestController
public class McpValidateController {
private final SessionTokenService sessionService;
private final UserRepository userRepo;
public McpValidateController(SessionTokenService sessionService,
UserRepository userRepo) {
this.sessionService = sessionService;
this.userRepo = userRepo;
}
@PostMapping("/api/mcp/validate")
public ResponseEntity<?> validate(
@RequestBody Map<String, String> body) {
String token = body.get("session_token");
if (token == null || token.isBlank()) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Missing session_token"));
}
var session = sessionService.findSession(token).orElse(null);
if (session == null) {
return ResponseEntity.ok(Map.of(
"valid", false, "remaining_seconds", 0));
}
// Verify user still exists
if (!userRepo.existsById(session.getUserId())) {
return ResponseEntity.ok(Map.of(
"valid", false, "remaining_seconds", 0));
}
long remaining = session.getExpiresAt().getEpochSecond()
- Instant.now().getEpochSecond();
List<String> roles = userRepo.findById(session.getUserId())
.map(u -> u.getRoles())
.orElse(List.of());
return ResponseEntity.ok(Map.of(
"valid", true,
"remaining_seconds", Math.max(0, remaining),
"roles", roles
));
}
}// routes/mcp-validate.ts
import { Router } from "express";
import { hmacMiddleware } from "../lib/mcp-contract/hmac";
import { findMcpSession } from "../lib/mcp-contract/session-token";
import { getUserRolesForMcp } from "../lib/mcp-contract/role-mapping";
import { prisma } from "../lib/prisma";
const router = Router();
router.post("/api/mcp/validate", hmacMiddleware, async (req, res) => {
const { session_token } = req.body as { session_token: string };
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
const session = await findMcpSession(session_token);
if (!session) {
return res.json({ valid: false, remaining_seconds: 0 });
}
const user = await prisma.user.findUnique({
where: { id: session.userId },
select: { id: true },
});
if (!user) {
return res.json({ valid: false, remaining_seconds: 0 });
}
const remainingMs = session.expiresAt.getTime() - Date.now();
const remainingSeconds = Math.floor(remainingMs / 1000);
const roles = await getUserRolesForMcp(session.userId);
return res.json({ valid: true, remaining_seconds: remainingSeconds, roles });
});
export default router;// routes/mcp-validate.js
const { Router } = require("express");
const { hmacMiddleware } = require("../lib/mcp-contract/hmac");
const { findMcpSession } = require("../lib/mcp-contract/session-token");
const { getUserRolesForMcp } = require("../lib/mcp-contract/role-mapping");
const { prisma } = require("../lib/prisma");
const router = Router();
router.post("/api/mcp/validate", hmacMiddleware, async (req, res) => {
const { session_token } = req.body;
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
const session = await findMcpSession(session_token);
if (!session) {
return res.json({ valid: false, remaining_seconds: 0 });
}
// Verify user still exists
const user = await prisma.user.findUnique({
where: { id: session.userId },
select: { id: true },
});
if (!user) {
return res.json({ valid: false, remaining_seconds: 0 });
}
const remainingMs = session.expiresAt.getTime() - Date.now();
const remainingSeconds = Math.floor(remainingMs / 1000);
const roles = await getUserRolesForMcp(session.userId);
return res.json({ valid: true, remaining_seconds: remainingSeconds, roles });
});
module.exports = router;# routers/mcp_validate.py
from datetime import datetime, timezone
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from mcp_contract.hmac_verify import verify_hub_signature
from mcp_contract.session_token import find_mcp_session
from your_db import get_db
from your_users import get_user_roles, user_exists
router = APIRouter()
class ValidateRequest(BaseModel):
session_token: str
@router.post("/api/mcp/validate", dependencies=[Depends(verify_hub_signature)])
async def mcp_validate(body: ValidateRequest, db=Depends(get_db)):
session = find_mcp_session(db, body.session_token)
if not session or not user_exists(db, session.user_id):
return {"valid": False, "remaining_seconds": 0}
remaining = (
session.expires_at - datetime.now(timezone.utc)
).total_seconds()
roles = get_user_roles(db, session.user_id)
return {
"valid": True,
"remaining_seconds": max(0, int(remaining)),
"roles": roles,
}POST /api/mcp/refresh Rotates a session token atomically: deletes the old token and creates a new one in a single transaction.
Request body:
{ "session_token": "mcp:old-token..." }Success response (200):
{ "session_token": "mcp:new-token...", "expires_in": 3600 }Error response (401):
{ "error": "session_expired" }Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/mcp/refresh/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { refreshMcpSession } from "@/lib/mcp-contract/session-token";
const SESSION_TTL_SEC = parseInt(
process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10);
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body: { session_token?: string };
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json(
{ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json(
{ error: "Missing session_token" }, { status: 400 });
}
const result = await refreshMcpSession(
session_token, SESSION_TTL_SEC);
if (!result) {
return NextResponse.json(
{ error: "session_expired" }, { status: 401 });
}
return NextResponse.json({
session_token: result.token,
expires_in: result.expiresIn,
});
}// app/api/mcp/refresh/route.js
import { NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { refreshMcpSession } from "@/lib/mcp-contract/session-token";
import { MCP_CONTRACT_SESSION_TTL_SEC } from "@/lib/mcp-contract/config";
export async function POST(request) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body;
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json({ error: "Missing session_token" }, { status: 400 });
}
const result = await refreshMcpSession(session_token, MCP_CONTRACT_SESSION_TTL_SEC);
if (!result) {
return NextResponse.json({ error: "session_expired" }, { status: 401 });
}
return NextResponse.json({
session_token: result.token, expires_in: result.expiresIn,
});
}// McpRefreshController.java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class McpRefreshController {
private final SessionTokenService sessionService;
private final McpContractProperties props;
public McpRefreshController(SessionTokenService sessionService,
McpContractProperties props) {
this.sessionService = sessionService;
this.props = props;
}
@PostMapping("/api/mcp/refresh")
public ResponseEntity<?> refresh(
@RequestBody Map<String, String> body) {
String token = body.get("session_token");
if (token == null || token.isBlank()) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Missing session_token"));
}
var result = sessionService.refreshSession(
token, props.sessionTtlSec());
if (result.isEmpty()) {
return ResponseEntity.status(401)
.body(Map.of("error", "session_expired"));
}
var tr = result.get();
return ResponseEntity.ok(Map.of(
"session_token", tr.token(),
"expires_in", tr.expiresIn()
));
}
}// routes/mcp-refresh.ts
import { Router } from "express";
import { hmacMiddleware } from "../lib/mcp-contract/hmac";
import { refreshMcpSession } from "../lib/mcp-contract/session-token";
import { MCP_CONTRACT_SESSION_TTL_SEC } from "../lib/mcp-contract/config";
const router = Router();
router.post("/api/mcp/refresh", hmacMiddleware, async (req, res) => {
const { session_token } = req.body as { session_token: string };
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
const result = await refreshMcpSession(session_token, MCP_CONTRACT_SESSION_TTL_SEC);
if (!result) {
return res.status(401).json({ error: "session_expired" });
}
return res.json({ session_token: result.token, expires_in: result.expiresIn });
});
export default router;// routes/mcp-refresh.js
const { Router } = require("express");
const { hmacMiddleware } = require("../lib/mcp-contract/hmac");
const { refreshMcpSession } = require("../lib/mcp-contract/session-token");
const { MCP_CONTRACT_SESSION_TTL_SEC } = require("../lib/mcp-contract/config");
const router = Router();
router.post("/api/mcp/refresh", hmacMiddleware, async (req, res) => {
const { session_token } = req.body;
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
const result = await refreshMcpSession(session_token, MCP_CONTRACT_SESSION_TTL_SEC);
if (!result) {
return res.status(401).json({ error: "session_expired" });
}
return res.json({ session_token: result.token, expires_in: result.expiresIn });
});
module.exports = router;# routers/mcp_refresh.py
import os
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from mcp_contract.hmac_verify import verify_hub_signature
from mcp_contract.session_token import refresh_mcp_session
from your_db import get_db
router = APIRouter()
SESSION_TTL_SEC = int(os.environ.get("MCP_CONTRACT_SESSION_TTL_SEC", "3600"))
class RefreshRequest(BaseModel):
session_token: str
@router.post("/api/mcp/refresh", dependencies=[Depends(verify_hub_signature)])
async def mcp_refresh(body: RefreshRequest, db=Depends(get_db)):
result = refresh_mcp_session(db, body.session_token, SESSION_TTL_SEC)
if not result:
raise HTTPException(401, detail="session_expired")
return {"session_token": result["token"], "expires_in": result["expires_in"]}POST /api/mcp/logout Revokes a session token (deletes it from storage).
Request body:
{ "session_token": "mcp:token-to-revoke..." }Response (200):
Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/mcp/logout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { revokeMcpSession } from "@/lib/mcp-contract/session-token";
export async function POST(request: NextRequest) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body: { session_token?: string };
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json(
{ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json(
{ error: "Missing session_token" }, { status: 400 });
}
await revokeMcpSession(session_token);
return NextResponse.json({ ok: true });
}// app/api/mcp/logout/route.js
import { NextResponse } from "next/server";
import { verifyHubSignature } from "@/lib/mcp-contract/hmac";
import { revokeMcpSession } from "@/lib/mcp-contract/session-token";
export async function POST(request) {
const rawBody = await request.text();
const hmacResult = await verifyHubSignature(
"POST", request.url, rawBody, request.headers);
if (!hmacResult.ok) {
return NextResponse.json(
{ error: hmacResult.error }, { status: hmacResult.status });
}
let body;
try { body = JSON.parse(rawBody); }
catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); }
const { session_token } = body;
if (!session_token) {
return NextResponse.json({ error: "Missing session_token" }, { status: 400 });
}
await revokeMcpSession(session_token);
return NextResponse.json({ ok: true });
}// McpLogoutController.java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class McpLogoutController {
private final SessionTokenService sessionService;
public McpLogoutController(SessionTokenService sessionService) {
this.sessionService = sessionService;
}
@PostMapping("/api/mcp/logout")
public ResponseEntity<?> logout(
@RequestBody Map<String, String> body) {
String token = body.get("session_token");
if (token == null || token.isBlank()) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Missing session_token"));
}
sessionService.revokeSession(token);
return ResponseEntity.ok(Map.of("ok", true));
}
}// routes/mcp-logout.ts
import { Router } from "express";
import { hmacMiddleware } from "../lib/mcp-contract/hmac";
import { revokeMcpSession } from "../lib/mcp-contract/session-token";
const router = Router();
router.post("/api/mcp/logout", hmacMiddleware, async (req, res) => {
const { session_token } = req.body as { session_token: string };
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
await revokeMcpSession(session_token);
return res.json({ ok: true });
});
export default router;// routes/mcp-logout.js
const { Router } = require("express");
const { hmacMiddleware } = require("../lib/mcp-contract/hmac");
const { revokeMcpSession } = require("../lib/mcp-contract/session-token");
const router = Router();
router.post("/api/mcp/logout", hmacMiddleware, async (req, res) => {
const { session_token } = req.body;
if (!session_token) {
return res.status(400).json({ error: "Missing session_token" });
}
await revokeMcpSession(session_token);
return res.json({ ok: true });
});
module.exports = router;# routers/mcp_logout.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from mcp_contract.hmac_verify import verify_hub_signature
from mcp_contract.session_token import revoke_mcp_session
from your_db import get_db
router = APIRouter()
class LogoutRequest(BaseModel):
session_token: str
@router.post("/api/mcp/logout", dependencies=[Depends(verify_hub_signature)])
async def mcp_logout(body: LogoutRequest, db=Depends(get_db)):
revoke_mcp_session(db, body.session_token)
return {"ok": True}GET /api/mcp/users Returns the list of active users in the tenant application. LockMCP calls this endpoint when a connection with a configured userListEndpoint path is created or updated. Synced users appear in the Members tab with source badge Auto and are assigned the tenant's default role.
HMAC: Signed when identity model is TENANT_DELEGATED (same headers as other server-to-server endpoints, empty-string body hash). Unauthenticated for other identity models — protect with IP allowlisting if needed.
Response (200):
[
{ "email": "alice@example.com", "name": "Alice Nguyen" },
{ "email": "bob@example.com", "displayName": "Bob Smith" }
]Top-level array or { "users": [...] } wrapper both accepted. Each entry requires an email field; name / displayName is optional.
Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/users/route.ts
// Configure this path as userListEndpoint on the LockMCP connection
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const users = await prisma.user.findMany({
where: { isActive: true },
select: { email: true, name: true },
orderBy: { name: "asc" },
});
return NextResponse.json(
users.map((u) => ({ email: u.email, name: u.name ?? u.email }))
);
}// app/api/users/route.js
// Configure this path as userListEndpoint on the LockMCP connection
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const users = await prisma.user.findMany({
where: { isActive: true },
select: { email: true, name: true },
orderBy: { name: "asc" },
});
return NextResponse.json(
users.map((u) => ({ email: u.email, name: u.name ?? u.email }))
);
}// McpUserListController.java
// Configure the mapping path as userListEndpoint on the LockMCP connection
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class McpUserListController {
private final UserRepository userRepo;
public McpUserListController(UserRepository userRepo) {
this.userRepo = userRepo;
}
@GetMapping("/api/users")
public ResponseEntity<List<UserSummary>> listUsers() {
var users = userRepo.findAllByActiveTrue().stream()
.map(u -> new UserSummary(u.getEmail(), u.getDisplayName()))
.toList();
return ResponseEntity.ok(users);
}
record UserSummary(String email, String name) {}
}// routes/mcp-users.ts
// Configure this path as userListEndpoint on the LockMCP connection
import { Router } from "express";
import { prisma } from "../lib/prisma";
const router = Router();
router.get("/api/mcp/users", async (_req, res) => {
const users = await prisma.user.findMany({
where: { isActive: true },
select: { email: true, name: true },
orderBy: { name: "asc" },
});
return res.json(users.map((u) => ({ email: u.email, name: u.name ?? u.email })));
});
export default router;// routes/mcp-users.js
// Configure this path as userListEndpoint on the LockMCP connection
const { Router } = require("express");
const { prisma } = require("../lib/prisma");
const router = Router();
router.get("/api/mcp/users", async (_req, res) => {
const users = await prisma.user.findMany({
where: { isActive: true },
select: { email: true, name: true },
orderBy: { name: "asc" },
});
return res.json(users.map((u) => ({ email: u.email, name: u.name ?? u.email })));
});
module.exports = router;# routers/mcp_users.py
# Configure this path as userListEndpoint on the LockMCP connection
from fastapi import APIRouter, Depends
from your_db import get_db
from your_users import get_active_users # replace with your user query
router = APIRouter()
@router.get("/api/mcp/users")
async def list_users(db=Depends(get_db)):
users = get_active_users(db)
return [
{"email": u.email, "name": u.display_name or u.email}
for u in users
]GET /api/mcp/roles Returns the list of roles defined in the tenant application. LockMCP calls this endpoint when a connection with a configured roleListEndpoint path is created or updated. Synced roles appear in the Roles tab and can be assigned to members or used for tool access control.
HMAC: Signed when identity model is TENANT_DELEGATED. Unauthenticated for other identity models — protect with IP allowlisting if needed.
Response (200):
[
{ "name": "admin", "description": "Full access to all features", "toolScopes": ["createCourse", "deleteCourse"] },
{ "name": "instructor", "description": "Can manage courses and students", "toolScopes": ["createCourse"] },
{ "name": "student", "description": "Can enrol and view courses", "toolScopes": [] }
]Top-level array or { "roles": [...] } wrapper both accepted. Each entry requires a name field; description and toolScopes are optional. toolScopes (contract 2+) lists the OpenAPI operationIds each role may invoke — see the Contract Specification .
Next.js (TypeScript) Next.js (JavaScript) Java Node.js (TypeScript) Node.js (JavaScript) Python
// app/api/roles/route.ts
// Configure this path as roleListEndpoint on the LockMCP connection
import { NextResponse } from "next/server";
// Define your tenant's roles here (or load from DB)
const TENANT_ROLES = [
{ name: "admin", description: "Full access to all features", toolScopes: ["createCourse", "deleteCourse"] },
{ name: "instructor", description: "Can manage courses and students", toolScopes: ["createCourse"] },
{ name: "student", description: "Can enrol and view courses", toolScopes: [] },
];
export async function GET() {
return NextResponse.json(TENANT_ROLES);
}// app/api/roles/route.js
// Configure this path as roleListEndpoint on the LockMCP connection
import { NextResponse } from "next/server";
const TENANT_ROLES = [
{ name: "admin", description: "Full access to all features", toolScopes: ["createCourse", "deleteCourse"] },
{ name: "instructor", description: "Can manage courses and students", toolScopes: ["createCourse"] },
{ name: "student", description: "Can enrol and view courses", toolScopes: [] },
];
export async function GET() {
return NextResponse.json(TENANT_ROLES);
}// McpRoleListController.java
// Configure the mapping path as roleListEndpoint on the LockMCP connection
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class McpRoleListController {
// Inject a RoleRepository if roles are stored in the DB
@GetMapping("/api/roles")
public ResponseEntity<List<Map<String, Object>>> listRoles() {
return ResponseEntity.ok(List.of(
Map.of("name", "admin",
"description", "Full access to all features",
"toolScopes", List.of("createCourse", "deleteCourse")),
Map.of("name", "instructor",
"description", "Can manage courses and students",
"toolScopes", List.of("createCourse")),
Map.of("name", "student",
"description", "Can enrol and view courses",
"toolScopes", List.of())
));
}
}// routes/mcp-roles.ts
// Configure this path as roleListEndpoint on the LockMCP connection
import { Router } from "express";
const router = Router();
const TENANT_ROLES = [
{ name: "admin", description: "Full access to all features", toolScopes: ["createCourse", "deleteCourse"] },
{ name: "instructor", description: "Can manage courses and students", toolScopes: ["createCourse"] },
{ name: "student", description: "Can enrol and view courses", toolScopes: [] },
];
router.get("/api/mcp/roles", (_req, res) => {
return res.json(TENANT_ROLES);
});
export default router;// routes/mcp-roles.js
// Configure this path as roleListEndpoint on the LockMCP connection
const { Router } = require("express");
const router = Router();
const TENANT_ROLES = [
{ name: "admin", description: "Full access to all features", toolScopes: ["createCourse", "deleteCourse"] },
{ name: "instructor", description: "Can manage courses and students", toolScopes: ["createCourse"] },
{ name: "student", description: "Can enrol and view courses", toolScopes: [] },
];
router.get("/api/mcp/roles", (_req, res) => {
return res.json(TENANT_ROLES);
});
module.exports = router;# routers/mcp_roles.py
# Configure this path as roleListEndpoint on the LockMCP connection
from fastapi import APIRouter
router = APIRouter()
# Define your tenant's roles here (or load from DB)
TENANT_ROLES = [
{"name": "admin", "description": "Full access to all features", "toolScopes": ["createCourse", "deleteCourse"]},
{"name": "instructor", "description": "Can manage courses and students", "toolScopes": ["createCourse"]},
{"name": "student", "description": "Can enrol and view courses", "toolScopes": []},
]
@router.get("/api/mcp/roles")
async def list_roles():
return TENANT_ROLESRouter registration (JavaScript) Wire the raw-body middleware once, then mount all MCP routers:
// app.js
const express = require("express");
const app = express();
// Capture raw body as string before any JSON parsing — required for HMAC verification
app.use(express.raw({ type: "*/*" }), (req, _res, next) => {
req.rawBody = req.body ? req.body.toString() : "";
next();
});
// Parse JSON and form bodies for route handlers (after raw body is captured)
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(require("./routes/mcp-login"));
app.use(require("./routes/mcp-exchange"));
app.use(require("./routes/mcp-validate"));
app.use(require("./routes/mcp-refresh"));
app.use(require("./routes/mcp-logout"));
app.use(require("./routes/mcp-users"));
app.use(require("./routes/mcp-roles"));
app.listen(3000);Router registration (Python) Register all MCP routers in your FastAPI app. The dependencies on each HMAC-protected route handle verification automatically.
# main.py
from fastapi import FastAPI
from routers import (
mcp_login, mcp_exchange, mcp_validate, mcp_refresh, mcp_logout,
mcp_users, mcp_roles,
)
app = FastAPI()
app.include_router(mcp_login.router)
app.include_router(mcp_exchange.router)
app.include_router(mcp_validate.router)
app.include_router(mcp_refresh.router)
app.include_router(mcp_logout.router)
app.include_router(mcp_users.router)
app.include_router(mcp_roles.router)Filter registration (Java) Register the HmacVerificationFilter for the 4 HMAC-protected endpoints:
// FilterConfig.java
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<HmacVerificationFilter> hmacFilter() {
var reg = new FilterRegistrationBean<>(
new HmacVerificationFilter());
reg.addUrlPatterns(
"/api/mcp/exchange",
"/api/mcp/validate",
"/api/mcp/refresh",
"/api/mcp/logout",
"/api/mcp/users",
"/api/mcp/roles"
);
reg.setOrder(1);
return reg;
}
}