源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
@@ -0,0 +1,391 @@
import type { EnterpriseMcpScenario } from "../contracts/scenario.js"
import type {
MutationOperationSummary,
RuntimeSnapshot,
SafeTraceDetail,
SafeTraceEvent,
EnterpriseMcpMockEnvironment,
} from "../contracts/runtime.js"
import type { HandshakePhase } from "../contracts/phases.js"
import type { ProviderProfile } from "../contracts/profile.js"
import type { FaultDefinition } from "../contracts/fault.js"
import { sanitizeTraceDetails, sha256 } from "../observability/redaction.js"
import { deepFreeze } from "../immutability.js"
export interface OAuthClientRecord {
readonly clientId: string
readonly clientSecret: string
readonly redirectUris: readonly string[]
readonly tokenEndpointAuthMethod: "none" | "client_secret_post"
readonly createdAt: number
readonly expiresAt: number | null
}
export interface AuthorizationCodeRecord {
readonly code: string
readonly clientId: string
readonly redirectUri: string
readonly codeChallenge: string
readonly resource: string
readonly scopes: readonly string[]
readonly expiresAt: number
}
export interface AccessTokenRecord {
readonly accessToken: string
readonly familyId: string
readonly clientId: string
readonly resource: string
readonly scopes: readonly string[]
readonly subject: string
readonly expiresAt: number
}
export interface RefreshTokenRecord {
readonly refreshToken: string
readonly familyId: string
readonly clientId: string
readonly resource: string
readonly scopes: readonly string[]
readonly subject: string
readonly expiresAt: number
}
export interface SessionRecord {
readonly sessionId: string
readonly tokenFamilyId: string
readonly operationNamespace: string
readonly profileId: string
readonly protocolVersion: string
readonly scenarioRevision: number
readonly expiresAt: number
initialized: boolean
}
interface MutableOperation {
readonly operationId: string
readonly idempotencyKeyHash: string
readonly namespaceHash: string
readonly tool: string
readonly argumentsHash: string
state: "prepared" | "committed" | "responded" | "indeterminate"
resultReference: string | null
readonly createdAt: number
}
export type PrepareOperationResult =
| { readonly kind: "prepared"; readonly operation: MutationOperationSummary }
| { readonly kind: "duplicate"; readonly operation: MutationOperationSummary }
| { readonly kind: "conflict"; readonly operation: MutationOperationSummary }
| { readonly kind: "reconcile"; readonly operation: MutationOperationSummary }
| { readonly kind: "capacity" }
interface EmitEventInput {
readonly correlationId: string
readonly scenario: EnterpriseMcpScenario
readonly phase: HandshakePhase
readonly direction: SafeTraceEvent["direction"]
readonly kind: SafeTraceEvent["kind"]
readonly outcome: SafeTraceEvent["outcome"]
readonly summary: string
readonly details?: Readonly<Record<string, SafeTraceDetail>>
}
const maximumEventCount = 500
const maximumClientCount = 100
const maximumAuthorizationCodeCount = 100
const maximumTokenCount = 100
const maximumSessionCount = 100
const maximumOperationCount = 1000
const maximumFaultCounterCount = 100
const operationTtlMs = 86_400_000
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
function canonicalJson(value: unknown): string {
if (value === null) return "null"
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") return JSON.stringify(value)
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`
if (isRecord(value)) {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
.join(",")}}`
}
return JSON.stringify(String(value))
}
export class InstanceState {
readonly instanceId: string
readonly clients = new Map<string, OAuthClientRecord>()
readonly authorizationCodes = new Map<string, AuthorizationCodeRecord>()
readonly tokens = new Map<string, AccessTokenRecord>()
readonly refreshTokens = new Map<string, RefreshTokenRecord>()
readonly sessions = new Map<string, SessionRecord>()
readonly operations = new Map<string, MutableOperation>()
readonly faultCounters = new Map<string, number>()
private readonly traceEvents: SafeTraceEvent[] = []
private runtimeStatus: RuntimeSnapshot["status"] = "idle"
private runtimeBaseUrl: string | null = null
constructor(
private currentScenario: EnterpriseMcpScenario,
private currentProfile: ProviderProfile,
private readonly knownSecrets: readonly string[],
private readonly environment: EnterpriseMcpMockEnvironment,
instanceId?: string,
) {
this.instanceId = instanceId ?? environment.randomId()
this.currentScenario = deepFreeze(currentScenario)
this.currentProfile = deepFreeze(currentProfile)
}
get scenario(): EnterpriseMcpScenario {
return this.currentScenario
}
get profile(): ProviderProfile {
return this.currentProfile
}
get status(): RuntimeSnapshot["status"] {
return this.runtimeStatus
}
get baseUrl(): string | null {
return this.runtimeBaseUrl
}
setRuntime(status: RuntimeSnapshot["status"], baseUrl: string | null): void {
this.runtimeStatus = status
this.runtimeBaseUrl = baseUrl
}
replaceScenario(scenario: EnterpriseMcpScenario, profile: ProviderProfile): void {
this.currentScenario = deepFreeze(scenario)
this.currentProfile = deepFreeze(profile)
this.clearEphemeralState()
}
clearEphemeralState(): void {
this.clearConnectionState()
this.operations.clear()
this.faultCounters.clear()
}
clearConnectionState(): void {
this.clients.clear()
this.authorizationCodes.clear()
this.tokens.clear()
this.refreshTokens.clear()
this.sessions.clear()
}
inheritEstablishedOAuthAuthority(source: InstanceState): void {
this.clearEphemeralState()
this.clearEvents()
source.maintainBounds(this.environment.now())
for (const [clientId, client] of source.clients) this.clients.set(clientId, client)
for (const [accessToken, token] of source.tokens) {
if (this.clients.has(token.clientId)) this.tokens.set(accessToken, token)
}
for (const [refreshToken, token] of source.refreshTokens) {
if (this.clients.has(token.clientId)) this.refreshTokens.set(refreshToken, token)
}
this.maintainBounds(this.environment.now())
}
maintainBounds(now = this.environment.now()): void {
for (const [key, client] of this.clients) {
if (client.expiresAt !== null && client.expiresAt < now) this.clients.delete(key)
}
for (const [key, code] of this.authorizationCodes) if (code.expiresAt < now) this.authorizationCodes.delete(key)
for (const [key, token] of this.tokens) if (token.expiresAt < now) this.tokens.delete(key)
for (const [key, token] of this.refreshTokens) if (token.expiresAt < now) this.refreshTokens.delete(key)
for (const [key, session] of this.sessions) if (session.expiresAt < now) this.sessions.delete(key)
for (const [key, operation] of this.operations) {
if (operation.state === "responded" && operation.createdAt + operationTtlMs < now) this.operations.delete(key)
}
this.trimOldest(this.clients, maximumClientCount)
this.trimOldest(this.authorizationCodes, maximumAuthorizationCodeCount)
this.trimOldest(this.tokens, maximumTokenCount)
this.trimOldest(this.refreshTokens, maximumTokenCount)
this.trimOldest(this.sessions, maximumSessionCount)
this.trimRespondedOperations(maximumOperationCount)
this.trimOldest(this.faultCounters, maximumFaultCounterCount)
}
putClient(client: OAuthClientRecord): void {
this.clients.set(client.clientId, client)
this.maintainBounds()
}
putAuthorizationCode(code: AuthorizationCodeRecord): void {
this.authorizationCodes.set(code.code, code)
this.maintainBounds()
}
putToken(token: AccessTokenRecord): void {
this.tokens.set(token.accessToken, token)
this.maintainBounds()
}
putRefreshToken(token: RefreshTokenRecord): void {
this.refreshTokens.set(token.refreshToken, token)
this.maintainBounds()
}
putSession(session: SessionRecord): void {
this.sessions.set(session.sessionId, session)
this.maintainBounds()
}
clearEvents(): void {
this.traceEvents.splice(0, this.traceEvents.length)
}
issueOpaque(prefix: string): string {
return this.environment.opaqueValue(prefix)
}
now(): number {
return this.environment.now()
}
emit(input: EmitEventInput): SafeTraceEvent {
const event: SafeTraceEvent = deepFreeze({
id: this.environment.randomId(),
occurredAt: new Date(this.environment.now()).toISOString(),
correlationId: input.correlationId,
revision: input.scenario.revision,
phase: input.phase,
direction: input.direction,
kind: input.kind,
outcome: input.outcome,
summary: input.summary,
details: sanitizeTraceDetails(input.details ?? {}, this.knownSecrets),
})
this.traceEvents.push(event)
if (this.traceEvents.length > maximumEventCount) this.traceEvents.splice(0, this.traceEvents.length - maximumEventCount)
return event
}
events(): readonly SafeTraceEvent[] {
return deepFreeze([...this.traceEvents])
}
shouldApplyFault(definition: FaultDefinition, scenario: EnterpriseMcpScenario): boolean {
const active = scenario.activeFault
if (!active || active.id !== definition.id) return false
const counterKey = `${scenario.revision}:${definition.id}`
const nextCount = (this.faultCounters.get(counterKey) ?? 0) + 1
this.faultCounters.set(counterKey, nextCount)
if (active.trigger.occurrence === "always") return true
if (active.trigger.occurrence === "once") return nextCount === 1
return nextCount === active.trigger.requestNumber
}
prepareOperation(namespace: string, tool: string, idempotencyKey: string, argumentsValue: unknown): PrepareOperationResult {
const namespaceHash = sha256(namespace)
const idempotencyKeyHash = sha256(idempotencyKey)
const argumentsHash = sha256(canonicalJson(argumentsValue))
const existing = [...this.operations.values()].find(
(operation) => operation.namespaceHash === namespaceHash && operation.idempotencyKeyHash === idempotencyKeyHash,
)
if (existing) {
const operation = this.operationSummary(existing)
if (existing.tool !== tool || existing.argumentsHash !== argumentsHash) return { kind: "conflict", operation }
if (existing.state !== "responded") return { kind: "reconcile", operation }
return { kind: "duplicate", operation }
}
this.maintainBounds()
if (this.operations.size >= maximumOperationCount) {
this.trimRespondedOperations(maximumOperationCount - 1)
if (this.operations.size >= maximumOperationCount) return { kind: "capacity" }
}
const operation: MutableOperation = {
operationId: this.issueOpaque("operation"),
idempotencyKeyHash,
namespaceHash,
tool,
argumentsHash,
state: "prepared",
resultReference: null,
createdAt: this.environment.now(),
}
this.operations.set(operation.operationId, operation)
this.maintainBounds()
return { kind: "prepared", operation: this.operationSummary(operation) }
}
transitionOperation(
operationId: string,
state: MutableOperation["state"],
resultReference: string | null,
): MutationOperationSummary {
const operation = this.operations.get(operationId)
if (!operation) throw new Error(`Unknown operation '${operationId}'`)
operation.state = state
operation.resultReference = resultReference
return this.operationSummary(operation)
}
snapshot(clientSecretConfigured: boolean): RuntimeSnapshot {
this.maintainBounds()
const baseUrl = this.runtimeBaseUrl
return deepFreeze({
status: this.runtimeStatus,
instanceId: this.instanceId,
scenario: this.currentScenario,
profile: this.currentProfile,
baseUrl,
mcpUrl: baseUrl ? new URL(this.currentProfile.endpointPath, baseUrl).href : null,
oauth: {
authorizationServerUrl: baseUrl,
protectedResourceMetadataUrl: baseUrl ? new URL(`/.well-known/oauth-protected-resource${this.currentProfile.endpointPath}`, baseUrl).href : null,
registration: this.currentScenario.oauth.registration,
clientId: this.currentScenario.oauth.clientId,
clientSecretConfigured,
},
counts: {
events: this.traceEvents.length,
sessions: this.sessions.size,
clients: this.clients.size,
tokens: this.tokens.size + this.refreshTokens.size,
operations: this.operations.size,
},
operations: [...this.operations.values()].map((operation) => this.operationSummary(operation)),
})
}
private operationSummary(operation: MutableOperation): MutationOperationSummary {
return {
operationId: operation.operationId,
idempotencyKeyHash: operation.idempotencyKeyHash,
tool: operation.tool,
argumentsHash: operation.argumentsHash,
state: operation.state,
resultReference: operation.resultReference,
}
}
private trimOldest<Key, Value>(map: Map<Key, Value>, maximum: number): void {
while (map.size > maximum) {
const oldestKey = map.keys().next().value
if (oldestKey === undefined) return
map.delete(oldestKey)
}
}
private trimRespondedOperations(maximum: number): void {
if (this.operations.size <= maximum) return
for (const [operationId, operation] of this.operations) {
if (operation.state === "responded") this.operations.delete(operationId)
if (this.operations.size <= maximum) return
}
}
}
@@ -0,0 +1,426 @@
import { randomBytes, randomUUID } from "node:crypto"
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
import type { Socket } from "node:net"
import { ZodError } from "zod"
import {
enterpriseMcpMockSecretsSchema,
scenarioCredentialContinuitySchema,
ScenarioCredentialContinuityError,
ScenarioRevisionConflictError,
} from "../contracts/runtime.js"
import type {
CreateEnterpriseMcpMockServerOptions,
EnterpriseMcpMockServer,
RuntimeSnapshot,
SafeTraceEvent,
EnterpriseMcpMockEnvironment,
UpdateScenarioOptions,
} from "../contracts/runtime.js"
import { scenarioSchema, type EnterpriseMcpScenario } from "../contracts/scenario.js"
import { getProviderProfile } from "../profiles/profiles.js"
import { getFaultDefinition } from "../faults/catalog.js"
import { InstanceState } from "./instance-state.js"
import { handleOAuthRequest } from "../protocol/oauth-handler.js"
import { handleMcpRequest } from "../protocol/mcp-handler.js"
import { HttpInputError, requestUrl, sendJson } from "../protocol/http-utils.js"
const defaultHost = "127.0.0.1"
const shutdownGraceMs = 500
function sameExactStrings(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length && left.every((value, index) => value === right[index])
}
function validateHost(host: string): void {
if (host !== "127.0.0.1" && host !== "::1") {
throw new Error("Enterprise MCP mock servers bind to loopback only")
}
}
function listen(server: Server, port: number, host: string): Promise<number> {
return new Promise((resolve, reject) => {
const onError = (error: Error): void => {
server.off("listening", onListening)
reject(error)
}
const onListening = (): void => {
server.off("error", onError)
const address = server.address()
if (!address || typeof address === "string") {
reject(new Error("Mock server did not expose a TCP address"))
return
}
resolve(address.port)
}
server.once("error", onError)
server.once("listening", onListening)
server.listen(port, host)
})
}
function close(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error)
else resolve()
})
})
}
class EnterpriseMcpMockServerRuntime implements EnterpriseMcpMockServer {
private readonly host: string
private readonly configuredPort: number
private readonly secrets: ReturnType<typeof enterpriseMcpMockSecretsSchema.parse>
private state: InstanceState
private server: Server | null = null
private sockets = new Set<Socket>()
private activeRequestCount = 0
private resolvedBaseUrl: string | null = null
private lifecycleTail: Promise<void> = Promise.resolve()
private listenAttempt = 0
private readonly environment: EnterpriseMcpMockEnvironment
constructor(options: CreateEnterpriseMcpMockServerOptions) {
this.host = options.host ?? defaultHost
this.configuredPort = options.port ?? 0
validateHost(this.host)
if (!Number.isInteger(this.configuredPort) || this.configuredPort < 0 || this.configuredPort > 65_535) {
throw new Error("Mock server port must be an integer from 0 through 65535")
}
const scenario = scenarioSchema.parse(options.scenario)
this.secrets = enterpriseMcpMockSecretsSchema.parse(options.secrets)
const profile = getProviderProfile(scenario.profileId)
if (
scenario.oauth.registration === "manual" &&
profile.oauth.defaultClientAuthenticationMethod === "client_secret_post" &&
this.secrets.oauthClientSecret.length < 12
) {
throw new Error(`Profile '${profile.id}' requires an OAuth client secret with at least 12 characters`)
}
this.environment =
options.environment ??
{
now: () => Date.now(),
randomId: () => randomUUID(),
opaqueValue: (prefix) => `${prefix}-${randomBytes(24).toString("base64url")}`,
}
this.state = this.createState(scenario)
}
get baseUrl(): string {
if (!this.resolvedBaseUrl) throw new Error("Enterprise MCP mock server has not started")
return this.resolvedBaseUrl
}
get mcpUrl(): string {
return new URL(this.state.profile.endpointPath, this.baseUrl).href
}
async start(): Promise<RuntimeSnapshot> {
return this.serializeLifecycle(() => this.startUnsafe())
}
async stop(): Promise<RuntimeSnapshot> {
return this.serializeLifecycle(async () => {
await this.stopListener()
this.state.clearConnectionState()
this.resolvedBaseUrl = null
this.state.setRuntime("stopped", null)
return this.snapshot()
})
}
async reset(): Promise<RuntimeSnapshot> {
return this.serializeLifecycle(async () => {
const wasRunning = this.server !== null
await this.stopListener()
this.state.clearEphemeralState()
this.state.clearEvents()
this.resolvedBaseUrl = null
this.state.setRuntime(wasRunning ? "idle" : "stopped", null)
return wasRunning ? this.startUnsafe() : this.snapshot()
})
}
async updateScenario(
nextValue: EnterpriseMcpScenario,
expectedRevision: number,
options: UpdateScenarioOptions = {},
): Promise<RuntimeSnapshot> {
return this.serializeLifecycle(async () => {
const currentRevision = this.state.scenario.revision
if (currentRevision !== expectedRevision) throw new ScenarioRevisionConflictError(expectedRevision, currentRevision)
const next = scenarioSchema.parse(nextValue)
if (next.revision !== expectedRevision + 1) {
throw new Error(`Next scenario revision must be ${expectedRevision + 1}`)
}
const parsedContinuity = scenarioCredentialContinuitySchema.safeParse(options.credentialContinuity ?? "reset")
if (!parsedContinuity.success) {
throw new ScenarioCredentialContinuityError("unsupported_mode", "Unsupported scenario credential-continuity mode.")
}
const credentialContinuity = parsedContinuity.data
if (credentialContinuity === "preserve-compatible-oauth") this.assertCompatibleOAuthContinuity(next)
const wasRunning = this.server !== null
const previousState = this.state
const previousBaseUrl = this.resolvedBaseUrl
await this.stopListener()
this.resolvedBaseUrl = null
const nextState = this.createState(next, previousState.instanceId)
if (credentialContinuity === "preserve-compatible-oauth") {
nextState.inheritEstablishedOAuthAuthority(previousState)
}
this.state = nextState
nextState.setRuntime(wasRunning ? "idle" : "stopped", null)
this.state.emit({
correlationId: this.environment.randomId(),
scenario: next,
phase: "CONFIGURATION",
direction: "internal",
kind: "lifecycle",
outcome: "completed",
summary: "Activated a new immutable mock scenario revision",
details: {
revision: next.revision,
profileId: next.profileId,
faultId: next.activeFault?.id ?? null,
credentialContinuity,
},
})
if (!wasRunning) return this.snapshot()
try {
const activated = await this.startUnsafe()
if (credentialContinuity === "preserve-compatible-oauth" && activated.baseUrl !== previousBaseUrl) {
throw new ScenarioCredentialContinuityError(
"incompatible_oauth_authority",
"The restarted data plane did not retain its exact OAuth resource URL.",
)
}
return activated
} catch (activationError) {
try {
await this.stopListener()
} catch {
// The rollback attempt below is the authoritative recovery result.
}
this.state = previousState
previousState.setRuntime("idle", null)
try {
await this.startUnsafe()
} catch (rollbackError) {
previousState.setRuntime("stopped", null)
throw new AggregateError(
[activationError, rollbackError],
"Scenario activation failed and the prior listener could not be restored",
)
}
throw activationError
}
})
}
snapshot(): RuntimeSnapshot {
return this.state.snapshot(
this.state.scenario.oauth.registration === "manual" &&
this.state.profile.oauth.defaultClientAuthenticationMethod === "client_secret_post" &&
this.secrets.oauthClientSecret.length > 0,
)
}
events(): readonly SafeTraceEvent[] {
return this.state.events()
}
private async startUnsafe(): Promise<RuntimeSnapshot> {
if (this.server) return this.snapshot()
this.seedManualClient()
const server = createServer((request, response) => {
// The development server can be restarted in place when a fault scenario
// changes. Make that boundary explicit to Node and Bun clients instead
// of leaving a reusable socket pointed at a retired listener generation.
response.setHeader("connection", "close")
this.activeRequestCount += 1
let finalized = false
const finalize = (): void => {
if (finalized) return
finalized = true
this.activeRequestCount = Math.max(0, this.activeRequestCount - 1)
}
response.once("finish", finalize)
response.once("close", finalize)
void this.route(request, response)
})
server.on("connection", (socket) => {
this.sockets.add(socket)
socket.once("close", () => this.sockets.delete(socket))
})
this.server = server
try {
this.listenAttempt += 1
await this.environment.beforeListen?.(this.listenAttempt)
const port = await listen(server, this.configuredPort, this.host)
const formattedHost = this.host === "::1" ? "[::1]" : this.host
this.resolvedBaseUrl = `http://${formattedHost}:${port}`
this.state.setRuntime("running", this.resolvedBaseUrl)
this.state.emit({
correlationId: this.environment.randomId(),
scenario: this.state.scenario,
phase: "CONFIGURATION",
direction: "internal",
kind: "lifecycle",
outcome: "completed",
summary: "Started an isolated enterprise MCP mock data plane",
details: { host: this.host, port, profileId: this.state.profile.id },
})
return this.snapshot()
} catch (error) {
this.server = null
this.resolvedBaseUrl = null
this.state.setRuntime("stopped", null)
throw error
}
}
private createState(scenario: EnterpriseMcpScenario, instanceId?: string): InstanceState {
const knownSecrets = [this.secrets.oauthClientSecret]
return new InstanceState(scenario, getProviderProfile(scenario.profileId), knownSecrets, this.environment, instanceId)
}
private assertCompatibleOAuthContinuity(next: EnterpriseMcpScenario): void {
if (this.configuredPort === 0) {
throw new ScenarioCredentialContinuityError(
"fixed_port_required",
"Preserving OAuth authority requires a fixed mock-server port so the protected resource URL cannot change.",
)
}
const current = this.state.scenario
const currentProfile = this.state.profile
const nextProfile = getProviderProfile(next.profileId)
const formattedHost = this.host === "::1" ? "[::1]" : this.host
const baseUrl = `http://${formattedHost}:${this.configuredPort}`
const currentEndpoint = new URL(currentProfile.endpointPath, baseUrl).href
const nextEndpoint = new URL(nextProfile.endpointPath, baseUrl).href
const compatible =
current.profileId === next.profileId &&
current.profileFixtureVersion === next.profileFixtureVersion &&
currentProfile.endpointPath === nextProfile.endpointPath &&
currentEndpoint === nextEndpoint &&
current.oauth.registration === next.oauth.registration &&
current.oauth.clientId === next.oauth.clientId &&
sameExactStrings(current.oauth.redirectUris, next.oauth.redirectUris) &&
sameExactStrings(current.oauth.authorizationScopes, next.oauth.authorizationScopes) &&
sameExactStrings(current.oauth.requiredResourceScopes, next.oauth.requiredResourceScopes)
if (!compatible) {
throw new ScenarioCredentialContinuityError(
"incompatible_oauth_authority",
"Cannot preserve OAuth authority because the provider, endpoint, resource, client registration, redirects, or scopes changed.",
)
}
}
private seedManualClient(): void {
const { scenario, profile } = this.state
if (scenario.oauth.registration !== "manual" || this.state.clients.has(scenario.oauth.clientId)) return
const tokenEndpointAuthMethod = profile.oauth.defaultClientAuthenticationMethod
this.state.putClient({
clientId: scenario.oauth.clientId,
clientSecret: tokenEndpointAuthMethod === "client_secret_post" ? this.secrets.oauthClientSecret : "",
redirectUris: scenario.oauth.redirectUris,
tokenEndpointAuthMethod,
createdAt: this.environment.now(),
expiresAt: null,
})
}
private async route(request: IncomingMessage, response: ServerResponse): Promise<void> {
const scenario = this.state.scenario
const profile = this.state.profile
const correlationId = this.environment.randomId()
this.state.maintainBounds()
try {
const baseUrl = this.baseUrl
const url = requestUrl(request, baseUrl)
if (request.method === "GET" && url.pathname === "/health") {
sendJson(response, 200, {
status: "ok",
instanceId: this.state.instanceId,
profileId: profile.id,
scenarioRevision: scenario.revision,
mcpUrl: new URL(profile.endpointPath, baseUrl).href,
})
return
}
const activeFault = scenario.activeFault ? getFaultDefinition(scenario.activeFault.id) : undefined
if (
await handleOAuthRequest({ request, response, url, baseUrl, correlationId, scenario, profile, state: this.state, activeFault })
) {
return
}
if (url.pathname === profile.endpointPath) {
await handleMcpRequest({ request, response, baseUrl, correlationId, scenario, profile, state: this.state, activeFault })
return
}
sendJson(response, 404, { error: "not_found" })
} catch (error) {
if (response.headersSent || response.destroyed) return
if (error instanceof HttpInputError) {
sendJson(response, error.status, { error: "invalid_http_input", message: error.message })
return
}
if (error instanceof ZodError) {
sendJson(response, 400, { error: "invalid_request", issues: error.issues.map((issue) => issue.message) })
return
}
this.state.emit({
correlationId,
scenario,
phase: "HTTP_ROUTING",
direction: "internal",
kind: "lifecycle",
outcome: "failed",
summary: "The mock server rejected an unexpected internal error safely",
})
sendJson(response, 500, { error: "mock_internal_error", diagnosticId: correlationId })
}
}
private async stopListener(): Promise<void> {
const server = this.server
if (!server) return
this.server = null
let closedGracefully = false
const closing = close(server).then(() => {
closedGracefully = true
})
let timeout: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<void>((resolve) => {
timeout = setTimeout(resolve, shutdownGraceMs)
timeout.unref()
})
await Promise.race([closing, deadline])
if (timeout) clearTimeout(timeout)
if (!closedGracefully && (this.activeRequestCount > 0 || this.sockets.size > 0)) {
for (const socket of this.sockets) socket.destroy()
}
await closing
// Let in-process HTTP clients observe the listener's FIN before a scenario
// restart reuses the same fixed OAuth resource port. This keeps normal
// keep-alive behavior while preventing one stale pooled request.
await new Promise<void>((resolve) => setImmediate(resolve))
this.sockets.clear()
this.activeRequestCount = 0
this.resolvedBaseUrl = null
}
private serializeLifecycle<Output>(operation: () => Promise<Output>): Promise<Output> {
const result = this.lifecycleTail.then(operation, operation)
this.lifecycleTail = result.then(
() => undefined,
() => undefined,
)
return result
}
}
export function createEnterpriseMcpMockServer(options: CreateEnterpriseMcpMockServerOptions): EnterpriseMcpMockServer {
return new EnterpriseMcpMockServerRuntime(options)
}