Merge branch 'feat/response-envelope-types'

This commit is contained in:
2026-05-11 01:55:23 +00:00
2 changed files with 157 additions and 1 deletions

View File

@@ -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";
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";

154
src/response-envelope.ts Normal file
View File

@@ -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<string, string>
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<string, unknown>
_meta?: Record<string, unknown>
}
export type ResponseMeta = LocalResponseMeta | HTTPResponseMeta | MCPResponseMeta
export interface ResponseEnvelope<T = unknown> {
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<string, unknown>
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<T>(data: T, operationId: string): ResponseEnvelope<T> {
return { data, meta: { source: "local", operationId, timestamp: Date.now() } }
}
export function httpEnvelope<T>(data: T, meta: Omit<HTTPResponseMeta, "source">): ResponseEnvelope<T> {
return { data, meta: { source: "http", ...meta } }
}
export function mcpEnvelope<T>(data: T, meta: Omit<MCPResponseMeta, "source">): ResponseEnvelope<T> {
return { data, meta: { source: "mcp", ...meta } }
}
export function unwrap<T>(envelope: ResponseEnvelope<T>): T {
return envelope.data
}