We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/andychoi/mcp-strapi'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
#!/usr/bin/env node
/**
* Local JWT Provider for Integration Testing
*
* Simulates an external OAuth/OIDC identity provider by:
* 1. Generating an RS256 key pair on startup
* 2. Serving a JWKS endpoint at http://localhost:{port}/.well-known/jwks.json
* 3. Signing JWTs with custom claims (email, role, tenant, permissions)
*
* This lets us test the full MCP JWT auth flow:
* MCP Client → JWT (signed by us) → MCP Auth Middleware → jose verifyJWT → JWKS (our server) → ✓
*
* Usage as a module:
* import { JwtProvider } from './jwt-provider.js';
* const provider = new JwtProvider({ port: 9876 });
* await provider.start();
* const token = await provider.sign({ email: 'user@test.local', role: 'Admin' });
* // ... use token in Authorization: Bearer header ...
* await provider.stop();
*
* Usage standalone (for debugging):
* node tests/jwt-provider.js
* # Serves JWKS at http://localhost:9876/.well-known/jwks.json
* # Prints a sample token to stdout
*/
import { createServer } from 'http';
import { generateKeyPair, exportJWK, SignJWT } from 'jose';
export class JwtProvider {
constructor(options = {}) {
this.port = options.port || 9876;
this.issuer = options.issuer || 'http://localhost:' + this.port;
this.audience = options.audience || 'mcp-test';
this.server = null;
this.privateKey = null;
this.publicKey = null;
this.publicJwk = null;
this.kid = 'test-key-1';
}
/**
* Generate RS256 key pair and start the JWKS HTTP server.
*/
async start() {
// Generate RS256 key pair
const { publicKey, privateKey } = await generateKeyPair('RS256');
this.publicKey = publicKey;
this.privateKey = privateKey;
// Export public key as JWK
const jwk = await exportJWK(publicKey);
jwk.kid = this.kid;
jwk.use = 'sig';
jwk.alg = 'RS256';
this.publicJwk = jwk;
// Start HTTP server serving JWKS
return new Promise((resolve, reject) => {
this.server = createServer((req, res) => {
// CORS headers for cross-origin JWKS fetch
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
if (req.url === '/.well-known/jwks.json' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
keys: [this.publicJwk],
}));
} else if (req.url === '/.well-known/openid-configuration' && req.method === 'GET') {
// Minimal OIDC discovery document
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
issuer: this.issuer,
jwks_uri: `${this.issuer}/.well-known/jwks.json`,
token_endpoint: `${this.issuer}/token`,
authorization_endpoint: `${this.issuer}/authorize`,
response_types_supported: ['code', 'token'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
}));
} else {
res.writeHead(404);
res.end('Not found');
}
});
this.server.listen(this.port, '127.0.0.1', () => {
resolve(this);
});
this.server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
// Port in use — try next port
this.port++;
this.issuer = `http://localhost:${this.port}`;
this.server.listen(this.port, '127.0.0.1');
} else {
reject(err);
}
});
});
}
/**
* Sign a JWT with the provider's private key.
*
* @param {Object} claims - Custom claims to include
* @param {string} claims.email - User email (required for MCP auth)
* @param {string} claims.role - MCP role: Admin, Publisher, Author, Reader
* @param {string} [claims.tenant] - Tenant ID for multi-tenant scoping
* @param {string[]} [claims.permissions] - Fine-grained permissions
* @param {string} [claims.sub] - Subject (defaults to email)
* @param {string} [expiresIn] - Expiry duration (default: '1h')
* @returns {Promise<string>} Signed JWT
*/
async sign(claims = {}, expiresIn = '1h') {
if (!this.privateKey) {
throw new Error('Provider not started. Call start() first.');
}
const now = Math.floor(Date.now() / 1000);
const jwt = new SignJWT({
email: claims.email,
role: claims.role || 'Reader',
tenant: claims.tenant,
permissions: claims.permissions || [],
...claims.extra, // any additional claims
})
.setProtectedHeader({ alg: 'RS256', kid: this.kid })
.setIssuedAt(now)
.setIssuer(this.issuer)
.setAudience(this.audience)
.setSubject(claims.sub || claims.email || 'test-subject')
.setExpirationTime(expiresIn);
return jwt.sign(this.privateKey);
}
/**
* Sign an expired JWT (for testing token expiry rejection).
*/
async signExpired(claims = {}) {
if (!this.privateKey) {
throw new Error('Provider not started. Call start() first.');
}
const past = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
const jwt = new SignJWT({
email: claims.email,
role: claims.role || 'Reader',
})
.setProtectedHeader({ alg: 'RS256', kid: this.kid })
.setIssuedAt(past - 3600)
.setIssuer(this.issuer)
.setAudience(this.audience)
.setSubject(claims.sub || claims.email || 'test-subject')
.setExpirationTime(past); // already expired
return jwt.sign(this.privateKey);
}
/**
* Sign a JWT with wrong issuer (for testing issuer mismatch).
*/
async signWrongIssuer(claims = {}) {
if (!this.privateKey) {
throw new Error('Provider not started. Call start() first.');
}
const jwt = new SignJWT({
email: claims.email,
role: claims.role || 'Reader',
})
.setProtectedHeader({ alg: 'RS256', kid: this.kid })
.setIssuedAt()
.setIssuer('https://wrong-issuer.example.com')
.setAudience(this.audience)
.setSubject(claims.sub || claims.email || 'test-subject')
.setExpirationTime('1h');
return jwt.sign(this.privateKey);
}
/**
* Sign a JWT with wrong audience.
*/
async signWrongAudience(claims = {}) {
if (!this.privateKey) {
throw new Error('Provider not started. Call start() first.');
}
const jwt = new SignJWT({
email: claims.email,
role: claims.role || 'Reader',
})
.setProtectedHeader({ alg: 'RS256', kid: this.kid })
.setIssuedAt()
.setIssuer(this.issuer)
.setAudience('wrong-audience')
.setSubject(claims.sub || claims.email || 'test-subject')
.setExpirationTime('1h');
return jwt.sign(this.privateKey);
}
/**
* Get the environment variables needed for Strapi MCP plugin JWT mode.
*/
getEnvVars() {
return {
JWT_JWKS_URI: `${this.issuer}/.well-known/jwks.json`,
JWT_ISSUER: this.issuer,
JWT_AUDIENCE: this.audience,
};
}
/**
* Get the JWKS URL for this provider.
*/
get jwksUri() {
return `${this.issuer}/.well-known/jwks.json`;
}
/**
* Stop the JWKS HTTP server.
*/
async stop() {
if (this.server) {
return new Promise((resolve) => {
this.server.close(resolve);
this.server = null;
});
}
}
}
// ─── Standalone mode: start server and print sample token ────────────────────
if (process.argv[1]?.endsWith('jwt-provider.js') && !process.argv[1]?.includes('run-all')) {
(async () => {
const provider = new JwtProvider();
await provider.start();
console.log(`JWT Provider running on port ${provider.port}`);
console.log(`JWKS endpoint: ${provider.jwksUri}`);
console.log();
console.log('Environment variables for Strapi:');
const env = provider.getEnvVars();
for (const [k, v] of Object.entries(env)) {
console.log(` ${k}=${v}`);
}
// Generate sample tokens
console.log('\nSample tokens:');
const adminToken = await provider.sign({ email: 'admin@example.com', role: 'Admin' });
console.log(`\n Admin: ${adminToken.substring(0, 60)}...`);
const authorToken = await provider.sign({ email: 'author@test.local', role: 'Author' });
console.log(` Author: ${authorToken.substring(0, 60)}...`);
const readerToken = await provider.sign({ email: 'reader@test.local', role: 'Reader' });
console.log(` Reader: ${readerToken.substring(0, 60)}...`);
const tenantToken = await provider.sign({ email: 'user@tenant.local', role: 'Author', tenant: 'acme-corp' });
console.log(` Tenant: ${tenantToken.substring(0, 60)}...`);
console.log('\nPress Ctrl+C to stop...');
})();
}