diff --git a/src/index.ts b/src/index.ts index 7ce6c7f..e3b2c1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,4 +15,6 @@ export { PendingRequestMap, buildCallHandler } from "./call.js"; export type { CallEventMap, CallEventMapValue, CallRequestedEvent, CallRespondedEvent, CallAbortedEvent, CallErrorEvent, CallHandler, CallHandlerConfig } from "./call.js"; export { subscribe } from "./subscribe.js"; export { createMCPClient, closeMCPClient, MCPClientLoader } from "./from_mcp.js"; -export type { MCPClientConfig, MCPClientWrapper } from "./from_mcp.js"; \ No newline at end of file +export type { MCPClientConfig, MCPClientWrapper } from "./from_mcp.js"; +export { ResponseEnvelopeSchema, ResponseMetaSchema, RESPONSE_SOURCES, isResponseEnvelope, localEnvelope, httpEnvelope, mcpEnvelope, unwrap } from "./response-envelope.js"; +export type { ResponseEnvelope, ResponseMeta, ResponseSource, LocalResponseMeta, HTTPResponseMeta, MCPResponseMeta, MCPContentBlock, MCPResourceContent, MCPAnnotations } from "./response-envelope.js"; \ No newline at end of file diff --git a/src/response-envelope.ts b/src/response-envelope.ts new file mode 100644 index 0000000..7e043f7 --- /dev/null +++ b/src/response-envelope.ts @@ -0,0 +1,154 @@ +import { Type, type Static } from "@alkdev/typebox"; + +export type ResponseSource = "local" | "http" | "mcp" + +export interface LocalResponseMeta { + source: "local" + operationId: string + timestamp: number +} + +export interface HTTPResponseMeta { + source: "http" + statusCode: number + headers: Record + contentType: string +} + +export type MCPContentBlock = + | { type: "text"; text: string; annotations?: MCPAnnotations } + | { type: "image"; data: string; mimeType: string; annotations?: MCPAnnotations } + | { type: "audio"; data: string; mimeType: string; annotations?: MCPAnnotations } + | { type: "resource"; resource: MCPResourceContent; annotations?: MCPAnnotations } + | { type: "resource_link"; uri: string; name: string; description?: string; mimeType?: string } + +export interface MCPResourceContent { + uri: string + mimeType?: string + text?: string + blob?: string +} + +export interface MCPAnnotations { + audience?: Array<"user" | "assistant"> + priority?: number + lastModified?: string +} + +export interface MCPResponseMeta { + source: "mcp" + isError: boolean + content: MCPContentBlock[] + structuredContent?: Record + _meta?: Record +} + +export type ResponseMeta = LocalResponseMeta | HTTPResponseMeta | MCPResponseMeta + +export interface ResponseEnvelope { + data: T + meta: ResponseMeta +} + +const LocalResponseMetaSchema = Type.Object({ + source: Type.Literal("local"), + operationId: Type.String(), + timestamp: Type.Number(), +}) + +const HTTPResponseMetaSchema = Type.Object({ + source: Type.Literal("http"), + statusCode: Type.Number(), + headers: Type.Record(Type.String(), Type.String()), + contentType: Type.String(), +}) + +const MCPAnnotationsSchema = Type.Object({ + audience: Type.Optional(Type.Array(Type.Union([Type.Literal("user"), Type.Literal("assistant")]))), + priority: Type.Optional(Type.Number()), + lastModified: Type.Optional(Type.String()), +}) + +const MCPResourceContentSchema = Type.Object({ + uri: Type.String(), + mimeType: Type.Optional(Type.String()), + text: Type.Optional(Type.String()), + blob: Type.Optional(Type.String()), +}) + +const MCPContentBlockSchema = Type.Union([ + Type.Object({ + type: Type.Literal("text"), + text: Type.String(), + annotations: Type.Optional(MCPAnnotationsSchema), + }), + Type.Object({ + type: Type.Literal("image"), + data: Type.String(), + mimeType: Type.String(), + annotations: Type.Optional(MCPAnnotationsSchema), + }), + Type.Object({ + type: Type.Literal("audio"), + data: Type.String(), + mimeType: Type.String(), + annotations: Type.Optional(MCPAnnotationsSchema), + }), + Type.Object({ + type: Type.Literal("resource"), + resource: MCPResourceContentSchema, + annotations: Type.Optional(MCPAnnotationsSchema), + }), + Type.Object({ + type: Type.Literal("resource_link"), + uri: Type.String(), + name: Type.String(), + description: Type.Optional(Type.String()), + mimeType: Type.Optional(Type.String()), + }), +]) + +const MCPResponseMetaSchema = Type.Object({ + source: Type.Literal("mcp"), + isError: Type.Boolean(), + content: Type.Array(MCPContentBlockSchema), + structuredContent: Type.Optional(Type.Record(Type.String(), Type.Unknown())), + _meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())), +}) + +export const ResponseMetaSchema = Type.Union([ + LocalResponseMetaSchema, + HTTPResponseMetaSchema, + MCPResponseMetaSchema, +]) + +export const ResponseEnvelopeSchema = Type.Object({ + data: Type.Unknown({ description: "Operation output" }), + meta: ResponseMetaSchema, +}) + +export const RESPONSE_SOURCES = ["local", "http", "mcp"] as const + +export function isResponseEnvelope(value: unknown): value is ResponseEnvelope { + if (typeof value !== "object" || value === null) return false + const obj = value as Record + if (!("data" in obj) || !("meta" in obj)) return false + if (typeof obj.meta !== "object" || obj.meta === null) return false + return RESPONSE_SOURCES.includes((obj.meta as ResponseMeta).source as ResponseSource) +} + +export function localEnvelope(data: T, operationId: string): ResponseEnvelope { + return { data, meta: { source: "local", operationId, timestamp: Date.now() } } +} + +export function httpEnvelope(data: T, meta: Omit): ResponseEnvelope { + return { data, meta: { source: "http", ...meta } } +} + +export function mcpEnvelope(data: T, meta: Omit): ResponseEnvelope { + return { data, meta: { source: "mcp", ...meta } } +} + +export function unwrap(envelope: ResponseEnvelope): T { + return envelope.data +} \ No newline at end of file