d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
502 lines
14 KiB
TypeScript
502 lines
14 KiB
TypeScript
import { z } from 'zod'
|
|
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
|
|
import type { McpToolSchema, McpToolSchemaProperty } from '@/lib/mcp/types'
|
|
|
|
const MAX_MCP_REFRESH_SERVER_IDS = 100
|
|
|
|
const dateStringSchema = z.preprocess(
|
|
(value) => (value instanceof Date ? value.toISOString() : value),
|
|
z.string()
|
|
)
|
|
|
|
const optionalStringFromNullableSchema = z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
z.string().optional()
|
|
)
|
|
|
|
const optionalDateStringFromNullableSchema = z.preprocess((value) => {
|
|
if (value instanceof Date) return value.toISOString()
|
|
return value === null ? undefined : value
|
|
}, z.string().optional())
|
|
|
|
const optionalNumberFromNullableSchema = z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
z.number().optional()
|
|
)
|
|
|
|
const optionalConnectionStatusFromNullableSchema = z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
z.enum(['connected', 'disconnected', 'error']).optional()
|
|
)
|
|
|
|
const optionalHeadersFromNullableSchema = z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
z.record(z.string(), z.string()).optional()
|
|
)
|
|
|
|
export const mcpTransportSchema = z.enum(['streamable-http'])
|
|
|
|
export const mcpAuthTypeSchema = z.enum(['none', 'headers', 'oauth'])
|
|
|
|
const consecutiveFailuresSchema = z.preprocess(
|
|
(value) => (typeof value === 'number' ? value : undefined),
|
|
z.number().default(0)
|
|
)
|
|
|
|
export const mcpServerStatusConfigSchema = z
|
|
.object({
|
|
consecutiveFailures: consecutiveFailuresSchema,
|
|
lastSuccessfulDiscovery: z.string().nullable().default(null),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpToolSchemaPropertySchema: z.ZodType<McpToolSchemaProperty> = z.lazy(() =>
|
|
z
|
|
.object({
|
|
type: z.union([z.string(), z.array(z.string())]).optional(),
|
|
description: z.string().optional(),
|
|
items: z
|
|
.union([mcpToolSchemaPropertySchema, z.array(mcpToolSchemaPropertySchema)])
|
|
.optional(),
|
|
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
|
|
required: z.array(z.string()).optional(),
|
|
enum: z.array(z.unknown()).optional(),
|
|
default: z.unknown().optional(),
|
|
})
|
|
.passthrough()
|
|
)
|
|
|
|
export const mcpToolInputSchema: z.ZodType<McpToolSchema> = z
|
|
.object({
|
|
type: z.literal('object'),
|
|
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
|
|
required: z.array(z.string()).optional(),
|
|
description: z.string().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpToolSchema = z.object({
|
|
name: z.string(),
|
|
description: z.string().optional(),
|
|
inputSchema: mcpToolInputSchema,
|
|
serverId: z.string(),
|
|
serverName: z.string(),
|
|
})
|
|
|
|
export const storedMcpToolSchema = z.object({
|
|
workflowId: z.string(),
|
|
workflowName: z.string(),
|
|
serverId: z.string(),
|
|
serverUrl: z.string().optional(),
|
|
toolName: z.string(),
|
|
schema: mcpToolInputSchema.optional(),
|
|
})
|
|
|
|
export const mcpServerSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
workspaceId: z.string(),
|
|
name: z.string(),
|
|
description: optionalStringFromNullableSchema,
|
|
transport: mcpTransportSchema,
|
|
authType: mcpAuthTypeSchema.optional(),
|
|
url: optionalStringFromNullableSchema,
|
|
timeout: optionalNumberFromNullableSchema,
|
|
retries: optionalNumberFromNullableSchema,
|
|
headers: optionalHeadersFromNullableSchema,
|
|
enabled: z.boolean(),
|
|
connectionStatus: optionalConnectionStatusFromNullableSchema,
|
|
lastError: z.string().nullable().optional(),
|
|
statusConfig: z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
mcpServerStatusConfigSchema.optional()
|
|
),
|
|
toolCount: optionalNumberFromNullableSchema,
|
|
lastToolsRefresh: optionalDateStringFromNullableSchema,
|
|
lastConnected: optionalDateStringFromNullableSchema,
|
|
createdAt: dateStringSchema,
|
|
updatedAt: dateStringSchema,
|
|
deletedAt: optionalDateStringFromNullableSchema,
|
|
oauthClientId: optionalStringFromNullableSchema,
|
|
hasOauthClientSecret: z.boolean().optional(),
|
|
})
|
|
.passthrough()
|
|
export type McpServer = z.output<typeof mcpServerSchema>
|
|
|
|
export const mcpWorkspaceQuerySchema = z.object({
|
|
workspaceId: z.string().min(1),
|
|
})
|
|
|
|
export const mcpServerIdParamsSchema = z.object({
|
|
id: z.string().min(1),
|
|
})
|
|
|
|
export const createMcpServerBodySchema = z
|
|
.object({
|
|
name: z.string().min(1),
|
|
description: z.string().optional(),
|
|
transport: mcpTransportSchema,
|
|
url: z.string().optional(),
|
|
authType: mcpAuthTypeSchema.optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
timeout: z.number().optional(),
|
|
retries: z.number().optional(),
|
|
enabled: z.boolean().optional(),
|
|
source: z.string().optional(),
|
|
workspaceId: z.string().optional(),
|
|
oauthClientId: z.string().nullable().optional(),
|
|
oauthClientSecret: z.string().nullable().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const updateMcpServerBodySchema = createMcpServerBodySchema.partial()
|
|
|
|
export const deleteMcpServerQuerySchema = mcpWorkspaceQuerySchema.extend({
|
|
serverId: z.string().min(1),
|
|
source: z.string().optional(),
|
|
})
|
|
|
|
export const deleteMcpServerByQuerySchema = z.object({
|
|
serverId: z.string().optional(),
|
|
source: z.string().optional(),
|
|
})
|
|
|
|
export const discoverMcpToolsQuerySchema = mcpWorkspaceQuerySchema.extend({
|
|
serverId: z.string().optional(),
|
|
refresh: z
|
|
.union([z.boolean(), z.enum(['true', 'false']).transform((value) => value === 'true')])
|
|
.optional(),
|
|
})
|
|
|
|
export const refreshMcpToolsBodySchema = z.object({
|
|
serverIds: z
|
|
.array(z.string().min(1))
|
|
.transform((serverIds) => [...new Set(serverIds)])
|
|
.pipe(
|
|
z
|
|
.array(z.string())
|
|
.max(
|
|
MAX_MCP_REFRESH_SERVER_IDS,
|
|
`At most ${MAX_MCP_REFRESH_SERVER_IDS} MCP servers can be refreshed at once`
|
|
)
|
|
),
|
|
})
|
|
|
|
export const mcpEventsQuerySchema = z.object({
|
|
workspaceId: z.string().min(1).nullable(),
|
|
})
|
|
|
|
export const mcpServeRouteParamsSchema = z.object({
|
|
serverId: z.string().min(1),
|
|
})
|
|
|
|
export const mcpToolDiscoveryQuerySchema = z.object({
|
|
serverId: z.string().optional(),
|
|
refresh: z.string().optional(),
|
|
})
|
|
|
|
export const mcpToolExecutionBodySchema = z
|
|
.object({
|
|
serverId: z.string().min(1),
|
|
toolName: z.string().min(1),
|
|
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
workflowId: z.string().optional(),
|
|
})
|
|
.passthrough()
|
|
export type McpToolExecutionBody = z.input<typeof mcpToolExecutionBodySchema>
|
|
|
|
export const mcpToolResultSchema = z
|
|
.object({
|
|
content: z.array(z.unknown()).optional(),
|
|
isError: z.boolean().optional(),
|
|
structuredContent: z.unknown().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpToolExecutionResultSchema = z.object({
|
|
success: z.boolean(),
|
|
output: mcpToolResultSchema.optional(),
|
|
error: z.string().optional(),
|
|
})
|
|
export type McpToolExecutionResult = z.output<typeof mcpToolExecutionResultSchema>
|
|
|
|
export const mcpJsonRpcRequestSchema = z
|
|
.object({
|
|
jsonrpc: z.literal('2.0'),
|
|
id: z.union([z.string(), z.number()]),
|
|
method: z.string().min(1),
|
|
params: z.unknown().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpJsonRpcNotificationSchema = z
|
|
.object({
|
|
jsonrpc: z.literal('2.0'),
|
|
method: z.string().min(1),
|
|
params: z.unknown().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpJsonRpcMessageSchema = z
|
|
.object({
|
|
jsonrpc: z.literal('2.0'),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpRequestBodySchema = z.union([
|
|
mcpJsonRpcMessageSchema,
|
|
z.array(mcpJsonRpcMessageSchema),
|
|
])
|
|
|
|
export const mcpToolCallParamsSchema = z
|
|
.object({
|
|
name: z.string().min(1),
|
|
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const mcpServerTestBodySchema = z
|
|
.object({
|
|
name: z.string().min(1),
|
|
transport: mcpTransportSchema,
|
|
url: z.string().optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
timeout: z.number().optional(),
|
|
workspaceId: z.string().optional(),
|
|
})
|
|
.passthrough()
|
|
export type McpServerTestBody = z.input<typeof mcpServerTestBodySchema>
|
|
|
|
export const mcpServerTestResultSchema = z.object({
|
|
success: z.boolean(),
|
|
message: z.string().optional(),
|
|
error: z.string().optional(),
|
|
authRequired: z.boolean().optional(),
|
|
authType: mcpAuthTypeSchema.optional(),
|
|
serverInfo: z
|
|
.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
})
|
|
.optional(),
|
|
negotiatedVersion: z.string().optional(),
|
|
supportedCapabilities: z.array(z.string()).optional(),
|
|
toolCount: z.number().optional(),
|
|
warnings: z.array(z.string()).optional(),
|
|
})
|
|
export type McpServerTestResult = z.output<typeof mcpServerTestResultSchema>
|
|
|
|
export const refreshMcpServerResultSchema = z.object({
|
|
status: z.enum(['connected', 'disconnected', 'error']),
|
|
toolCount: z.number(),
|
|
lastConnected: z.string().nullable(),
|
|
error: z.string().nullable(),
|
|
workflowsUpdated: z.number(),
|
|
updatedWorkflowIds: z.array(z.string()),
|
|
})
|
|
export type RefreshMcpServerResult = z.output<typeof refreshMcpServerResultSchema>
|
|
|
|
const mcpSuccessResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
|
|
z.object({
|
|
success: z.literal(true),
|
|
data: dataSchema,
|
|
})
|
|
|
|
export const listMcpServersContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/mcp/servers',
|
|
query: mcpWorkspaceQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
servers: z.array(mcpServerSchema),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
export type ListMcpServersResponse = ContractJsonResponse<typeof listMcpServersContract>
|
|
|
|
export const createMcpServerContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/mcp/servers',
|
|
body: createMcpServerBodySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
serverId: z.string(),
|
|
updated: z.boolean().optional(),
|
|
authType: mcpAuthTypeSchema.optional(),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
|
|
export const deleteMcpServerContract = defineRouteContract({
|
|
method: 'DELETE',
|
|
path: '/api/mcp/servers',
|
|
query: deleteMcpServerQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
message: z.string(),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
|
|
export const updateMcpServerContract = defineRouteContract({
|
|
method: 'PATCH',
|
|
path: '/api/mcp/servers/[id]',
|
|
params: mcpServerIdParamsSchema,
|
|
query: mcpWorkspaceQuerySchema,
|
|
body: updateMcpServerBodySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
server: mcpServerSchema,
|
|
})
|
|
),
|
|
},
|
|
})
|
|
|
|
export const refreshMcpServerContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/mcp/servers/[id]/refresh',
|
|
params: mcpServerIdParamsSchema,
|
|
query: mcpWorkspaceQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(refreshMcpServerResultSchema),
|
|
},
|
|
})
|
|
|
|
export const discoverMcpToolsContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/mcp/tools/discover',
|
|
query: discoverMcpToolsQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
tools: z.array(mcpToolSchema),
|
|
totalCount: z.number(),
|
|
byServer: z.record(z.string(), z.number()),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
export type DiscoverMcpToolsResponse = ContractJsonResponse<typeof discoverMcpToolsContract>
|
|
|
|
export const refreshMcpToolsContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/mcp/tools/discover',
|
|
query: mcpWorkspaceQuerySchema,
|
|
body: refreshMcpToolsBodySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
refreshed: z.array(z.object({ serverId: z.string(), toolCount: z.number() })),
|
|
failed: z.array(z.object({ serverId: z.string(), error: z.string() })),
|
|
summary: z.object({
|
|
total: z.number(),
|
|
successful: z.number(),
|
|
failed: z.number(),
|
|
}),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
|
|
export const listStoredMcpToolsContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/mcp/tools/stored',
|
|
query: mcpWorkspaceQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(
|
|
z.object({
|
|
tools: z.array(storedMcpToolSchema),
|
|
})
|
|
),
|
|
},
|
|
})
|
|
|
|
export const testMcpServerConnectionContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/mcp/servers/test-connection',
|
|
body: mcpServerTestBodySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(mcpServerTestResultSchema),
|
|
},
|
|
})
|
|
|
|
export const executeMcpToolContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/mcp/tools/execute',
|
|
body: mcpToolExecutionBodySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: mcpSuccessResponseSchema(mcpToolExecutionResultSchema),
|
|
},
|
|
})
|
|
export type ExecuteMcpToolResponse = ContractJsonResponse<typeof executeMcpToolContract>
|
|
|
|
export const startMcpOauthQuerySchema = z.object({
|
|
serverId: z.string().min(1, 'serverId is required'),
|
|
workspaceId: z.string().min(1, 'workspaceId is required'),
|
|
})
|
|
|
|
export const startMcpOauthResultSchema = z.discriminatedUnion('status', [
|
|
z.object({ status: z.literal('redirect'), authorizationUrl: z.string().url() }),
|
|
z.object({ status: z.literal('already_authorized') }),
|
|
])
|
|
export type StartMcpOauthResult = z.output<typeof startMcpOauthResultSchema>
|
|
|
|
export const startMcpOauthContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/mcp/oauth/start',
|
|
query: startMcpOauthQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: startMcpOauthResultSchema,
|
|
},
|
|
})
|
|
|
|
/**
|
|
* Provider can return any subset depending on the outcome:
|
|
* - success: `state` + `code`
|
|
* - provider error: `error` + optional `error_description` + optional `state`
|
|
* - malformed callback: nothing
|
|
* All fields are optional so the route can render an HTML error page itself.
|
|
*/
|
|
export const mcpOauthCallbackQuerySchema = z.object({
|
|
state: z.string().optional(),
|
|
code: z.string().optional(),
|
|
error: z.string().optional(),
|
|
error_description: z.string().optional(),
|
|
})
|
|
|
|
export const mcpOauthCallbackContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/mcp/oauth/callback',
|
|
query: mcpOauthCallbackQuerySchema,
|
|
response: { mode: 'text' },
|
|
})
|
|
|
|
export const getAllowedMcpDomainsContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/settings/allowed-mcp-domains',
|
|
response: {
|
|
mode: 'json',
|
|
schema: z.object({
|
|
allowedMcpDomains: z.array(z.string()).nullable(),
|
|
}),
|
|
},
|
|
})
|