maestro/src/mcp/registry.ts
oss-sync 8041bc62cc
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (03a03e4)
2026-06-09 14:04:19 +00:00

230 lines
8.0 KiB
TypeScript

import type Database from 'better-sqlite3';
import { encrypt, decrypt, loadKeyFromEnv } from './crypto.js';
import type {
AuthKind,
McpServerPublic,
McpServerRecord,
McpDiscoveryMetadata,
} from './types.js';
import { logger } from '../logger.js';
const ID_REGEX = /^[a-z0-9_-]{1,64}$/;
// RFC 7230 header field-name token chars. Guards against header injection
// (CR/LF, colon, spaces) when a custom auth header name is supplied.
const HEADER_NAME_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
export interface UpsertInput {
id: string;
name: string;
url: string;
authKind: AuthKind;
ownerId: string | null; // null = global/admin-managed
// OAuth fields (required when authKind === 'oauth')
oauthClientId?: string;
oauthClientSecret?: string;
oauthScopes?: string | null;
// API key field (required when authKind === 'api_key')
staticToken?: string;
// Optional custom auth header name for api_key servers (e.g. 'xc-mcp-token').
authHeaderName?: string | null;
enabled?: boolean;
createdBy?: string | null;
}
interface Row {
id: string;
name: string;
url: string;
oauth_client_id: string;
oauth_client_secret_enc: Buffer;
oauth_scopes: string | null;
issuer: string | null;
authorization_endpoint: string | null;
token_endpoint: string | null;
discovery_fingerprint: string | null;
enabled: number;
created_by: string | null;
created_at: string;
updated_at: string;
// Phase 8 columns
auth_kind: string;
static_token_enc: Buffer | null;
owner_id: string | null;
auth_header_name: string | null;
}
function rowToPublic(r: Row): McpServerPublic {
return {
id: r.id,
name: r.name,
url: r.url,
authKind: (r.auth_kind ?? 'oauth') as AuthKind,
ownerId: r.owner_id ?? null,
authHeaderName: r.auth_header_name ?? null,
oauthClientId: r.oauth_client_id,
oauthScopes: r.oauth_scopes,
issuer: r.issuer,
authorizationEndpoint: r.authorization_endpoint,
tokenEndpoint: r.token_endpoint,
discoveryFingerprint: r.discovery_fingerprint,
enabled: r.enabled === 1,
createdBy: r.created_by,
createdAt: r.created_at,
updatedAt: r.updated_at,
};
}
export function createRegistry(db: Database.Database) {
return {
upsert(input: UpsertInput): void {
if (!ID_REGEX.test(input.id)) {
throw new Error(`id must match ${ID_REGEX}: got "${input.id}"`);
}
const key = loadKeyFromEnv();
const authKind = input.authKind;
// Validate per-kind requirements
if (authKind === 'oauth') {
if (!input.oauthClientId || !input.oauthClientSecret) {
throw new Error(`authKind 'oauth' requires oauthClientId and oauthClientSecret`);
}
} else if (authKind === 'api_key') {
if (!input.staticToken) {
throw new Error(`authKind 'api_key' requires staticToken`);
}
} else {
throw new Error(`Unknown authKind: ${authKind as string}`);
}
// Optional custom auth header name (api_key only). Validated against the
// RFC 7230 token grammar to prevent header injection.
const authHeaderName = input.authHeaderName?.trim() || null;
if (authHeaderName !== null) {
if (authKind !== 'api_key') {
throw new Error(`authHeaderName is only valid for authKind 'api_key'`);
}
if (!HEADER_NAME_REGEX.test(authHeaderName)) {
throw new Error(`authHeaderName must be a valid HTTP header token: got "${authHeaderName}"`);
}
}
// For oauth: encrypt the real secret; static_token_enc stays NULL.
// For api_key: oauth_client_id = '' and oauth_client_secret_enc = encrypt('', key)
// because those columns are NOT NULL in the original schema. They are never read
// for api_key servers. static_token_enc = encrypt(realToken, key).
const oauthClientId = authKind === 'oauth' ? input.oauthClientId! : '';
const oauthSecretEnc = encrypt(authKind === 'oauth' ? input.oauthClientSecret! : '', key);
const staticTokenEnc = authKind === 'api_key' ? encrypt(input.staticToken!, key) : null;
db.prepare(
`INSERT INTO mcp_servers
(id, name, url, oauth_client_id, oauth_client_secret_enc, oauth_scopes,
auth_kind, owner_id, static_token_enc, auth_header_name, enabled, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name,
url=excluded.url,
oauth_client_id=excluded.oauth_client_id,
oauth_client_secret_enc=excluded.oauth_client_secret_enc,
oauth_scopes=excluded.oauth_scopes,
auth_kind=excluded.auth_kind,
owner_id=excluded.owner_id,
static_token_enc=excluded.static_token_enc,
auth_header_name=excluded.auth_header_name,
enabled=excluded.enabled,
updated_at=datetime('now')`,
).run(
input.id,
input.name,
input.url,
oauthClientId,
oauthSecretEnc,
input.oauthScopes ?? null,
authKind,
input.ownerId ?? null,
staticTokenEnc,
authHeaderName,
input.enabled === false ? 0 : 1,
input.createdBy ?? null,
);
logger.info(`[mcp:registry] upsert server id=${input.id} url=${input.url} authKind=${authKind}`);
},
delete(id: string): void {
db.prepare('DELETE FROM mcp_servers WHERE id = ?').run(id);
logger.info(`[mcp:registry] delete server id=${id}`);
},
listPublic(): McpServerPublic[] {
const rows = db.prepare('SELECT * FROM mcp_servers ORDER BY id').all() as Row[];
return rows.map(rowToPublic);
},
/**
* Returns all enabled servers visible to the given user:
* - Global (owner_id IS NULL) servers are visible to everyone.
* - User-owned (owner_id = userId) servers are only visible to that user.
*/
listEnabledForUser(userId: string): McpServerPublic[] {
const rows = db
.prepare('SELECT * FROM mcp_servers WHERE enabled = 1 AND (owner_id IS NULL OR owner_id = ?) ORDER BY id')
.all(userId) as Row[];
return rows.map(rowToPublic);
},
/**
* Returns only servers owned by the given user (for owner/admin scoped listing).
*/
listEnabledForOwner(ownerId: string): McpServerPublic[] {
const rows = db
.prepare('SELECT * FROM mcp_servers WHERE enabled = 1 AND owner_id = ? ORDER BY id')
.all(ownerId) as Row[];
return rows.map(rowToPublic);
},
/** @deprecated Use listEnabledForUser(userId) to include user-owned servers. */
listEnabledPublic(): McpServerPublic[] {
const rows = db
.prepare('SELECT * FROM mcp_servers WHERE enabled = 1 ORDER BY id')
.all() as Row[];
return rows.map(rowToPublic);
},
getDecrypted(id: string): McpServerRecord | null {
const row = db.prepare('SELECT * FROM mcp_servers WHERE id = ?').get(id) as
| Row
| undefined;
if (!row) return null;
const key = loadKeyFromEnv();
const authKind = (row.auth_kind ?? 'oauth') as AuthKind;
const oauthClientSecret = authKind === 'oauth'
? decrypt(row.oauth_client_secret_enc, key)
: ''; // not used for api_key servers
const staticToken = (authKind === 'api_key' && row.static_token_enc)
? decrypt(row.static_token_enc, key)
: null;
return {
...rowToPublic(row),
oauthClientSecret,
staticToken,
};
},
setDiscovery(id: string, meta: McpDiscoveryMetadata): void {
db.prepare(
`UPDATE mcp_servers
SET issuer=?, authorization_endpoint=?, token_endpoint=?, discovery_fingerprint=?, updated_at=datetime('now')
WHERE id=?`,
).run(meta.issuer, meta.authorizationEndpoint, meta.tokenEndpoint, meta.fingerprint, id);
},
setEnabled(id: string, enabled: boolean): void {
db.prepare(
`UPDATE mcp_servers SET enabled=?, updated_at=datetime('now') WHERE id=?`,
).run(enabled ? 1 : 0, id);
},
};
}
export type McpRegistry = ReturnType<typeof createRegistry>;