Initial package implementation: operations registry, call protocol, and adapters

Extracted from alkhub_ts packages/core/operations/ and packages/core/mcp/.
- Runtime-agnostic (injected fs/env deps, no Deno globals)
- Direct @logtape/logtape import instead of logger wrapper
- PendingRequestMap with pubsub-wired call protocol
- Peer-dep isolation for MCP adapter (sub-path export)
- Schema const naming convention (XSchema + X type alias)
- 68 tests passing, build + lint + test all green
This commit is contained in:
2026-04-30 12:34:26 +00:00
parent 9c41f683ee
commit 29f0dd7af0
37 changed files with 9287 additions and 0 deletions

249
src/call.ts Normal file
View File

@@ -0,0 +1,249 @@
import { Type, type Static } from "@alkdev/typebox";
import { createPubSub, type PubSub } from "@alkdev/pubsub";
import { getLogger } from "@logtape/logtape";
import { OperationRegistry } from "./registry.js";
import { CallError, InfrastructureErrorCode, mapError } from "./error.js";
import { validateOrThrow } from "./validation.js";
import type { IOperationDefinition, Identity, OperationContext, AccessControl } from "./types.js";
const logger = getLogger("operations:call");
export const CallEventSchema = {
"call.requested": Type.Object({
requestId: Type.String(),
operationId: Type.String(),
input: Type.Unknown(),
parentRequestId: Type.Optional(Type.String()),
deadline: Type.Optional(Type.Number()),
identity: Type.Optional(Type.Object({
id: Type.String(),
scopes: Type.Array(Type.String()),
resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))),
})),
}),
"call.responded": Type.Object({
requestId: Type.String(),
output: Type.Unknown(),
}),
"call.aborted": Type.Object({
requestId: Type.String(),
}),
"call.error": Type.Object({
requestId: Type.String(),
code: Type.String(),
message: Type.String(),
details: Type.Optional(Type.Unknown()),
}),
} as const;
export type CallRequestedEvent = Static<typeof CallEventSchema["call.requested"]>;
export type CallRespondedEvent = Static<typeof CallEventSchema["call.responded"]>;
export type CallAbortedEvent = Static<typeof CallEventSchema["call.aborted"]>;
export type CallErrorEvent = Static<typeof CallEventSchema["call.error"]>;
export type CallEventMapValue = CallRequestedEvent | CallRespondedEvent | CallAbortedEvent | CallErrorEvent;
export const CallEventMap = CallEventSchema;
type CallPubSubMap = {
"call.requested": [CallRequestedEvent];
"call.responded": [CallRespondedEvent];
"call.aborted": [CallAbortedEvent];
"call.error": [CallErrorEvent];
};
interface PendingRequest {
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
deadline?: number;
timer?: ReturnType<typeof setTimeout>;
}
export interface CallHandlerConfig {
registry: OperationRegistry;
eventTarget?: EventTarget;
}
export type CallHandler = (event: CallRequestedEvent) => Promise<void>;
export class PendingRequestMap {
private requests = new Map<string, PendingRequest>();
private pubsub: PubSub<CallPubSubMap>;
constructor(eventTarget?: EventTarget) {
this.pubsub = createPubSub<CallPubSubMap>(
eventTarget ? { eventTarget: eventTarget as any } : undefined
);
this.setupSubscriptions();
}
private setupSubscriptions(): void {
const respondedIter = this.pubsub.subscribe("call.responded");
(async () => {
for await (const event of respondedIter) {
const responded = event as CallRespondedEvent;
const pending = this.requests.get(responded.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(responded.requestId);
pending.resolve(responded.output);
}
}
})();
const errorIter = this.pubsub.subscribe("call.error");
(async () => {
for await (const event of errorIter) {
const err = event as CallErrorEvent;
const pending = this.requests.get(err.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(err.requestId);
pending.reject(new CallError(err.code, err.message, err.details));
}
}
})();
const abortedIter = this.pubsub.subscribe("call.aborted");
(async () => {
for await (const event of abortedIter) {
const aborted = event as CallAbortedEvent;
const pending = this.requests.get(aborted.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(aborted.requestId);
pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${aborted.requestId} was aborted`));
}
}
})();
}
async call(
operationId: string,
input: unknown,
options?: { parentRequestId?: string; deadline?: number; identity?: Identity },
): Promise<unknown> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
const pending: PendingRequest = { resolve, reject };
if (options?.deadline) {
pending.deadline = options.deadline;
pending.timer = setTimeout(() => {
this.requests.delete(requestId);
reject(new CallError(InfrastructureErrorCode.TIMEOUT, `Request ${requestId} timed out`, { deadline: options.deadline }));
}, options.deadline - Date.now());
}
this.requests.set(requestId, pending);
this.pubsub.publish("call.requested", {
requestId,
operationId,
input,
parentRequestId: options?.parentRequestId,
deadline: options?.deadline,
identity: options?.identity,
});
});
}
respond(requestId: string, output: unknown): void {
this.pubsub.publish("call.responded", {
requestId,
output,
});
}
emitError(requestId: string, code: string, message: string, details?: unknown): void {
this.pubsub.publish("call.error", {
requestId,
code,
message,
details,
});
}
abort(requestId: string): void {
const pending = this.requests.get(requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(requestId);
this.pubsub.publish("call.aborted", { requestId });
pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${requestId} was aborted`));
}
}
getPendingCount(): number {
return this.requests.size;
}
}
export function buildCallHandler(config: CallHandlerConfig): CallHandler {
const { registry } = config;
return async (event: CallRequestedEvent): Promise<void> => {
const { requestId, operationId, input, identity } = event;
try {
const operation = registry.get(operationId);
if (!operation) {
throw new CallError(
InfrastructureErrorCode.OPERATION_NOT_FOUND,
`Operation not found: ${operationId}`,
{ operationId },
);
}
const accessControl: AccessControl = operation.accessControl as AccessControl;
if (identity && !checkAccess(accessControl, identity)) {
throw new CallError(
InfrastructureErrorCode.ACCESS_DENIED,
`Access denied for operation: ${operationId}`,
{ requiredScopes: accessControl.requiredScopes },
);
}
const context: OperationContext = {
requestId,
parentRequestId: event.parentRequestId,
identity,
};
validateOrThrow(operation.inputSchema, input, `Input validation for ${operationId}`);
await operation.handler(input, context);
} catch (error) {
const callError = mapError(error);
throw callError;
}
};
}
function checkAccess(accessControl: AccessControl, identity: Identity): boolean {
const { requiredScopes, requiredScopesAny, resourceType, resourceAction } = accessControl;
if (requiredScopes.length > 0) {
const hasAll = requiredScopes.every((scope: string) => identity.scopes.includes(scope));
if (!hasAll) return false;
}
if (requiredScopesAny && requiredScopesAny.length > 0) {
const hasAny = requiredScopesAny.some((scope: string) => identity.scopes.includes(scope));
if (!hasAny) return false;
}
if (resourceType && resourceAction && identity.resources) {
for (const [key, actions] of Object.entries(identity.resources)) {
if (key.startsWith(`${resourceType}:`) && actions.includes(resourceAction)) {
return true;
}
}
return false;
}
return true;
}

56
src/env.ts Normal file
View File

@@ -0,0 +1,56 @@
import { OperationType } from "./types.js";
import type { OperationContext, OperationEnv } from "./types.js";
import type { OperationRegistry } from "./registry.js";
import { getLogger } from "@logtape/logtape";
const logger = getLogger("operations:env");
export interface PendingRequestMap {
call(operationId: string, input: unknown, options?: { parentRequestId?: string; identity?: unknown }): Promise<unknown>;
}
export interface EnvOptions {
registry: OperationRegistry;
context: OperationContext;
allowedNamespaces?: string[];
callMap?: PendingRequestMap;
}
export function buildEnv(options: EnvOptions): OperationEnv {
const { registry, context, allowedNamespaces, callMap } = options;
const operations = registry.list();
const namespaces: OperationEnv = {};
for (const operation of operations) {
if (allowedNamespaces && !allowedNamespaces.includes(operation.namespace)) {
continue;
}
if (operation.type === OperationType.SUBSCRIPTION) {
continue;
}
if (!namespaces[operation.namespace]) {
namespaces[operation.namespace] = {};
}
const operationId = `${operation.namespace}.${operation.name}`;
if (callMap) {
namespaces[operation.namespace][operation.name] = async (input: unknown) => {
logger.debug(`Call protocol: ${operationId}`);
return await callMap.call(operationId, input, {
parentRequestId: context.requestId,
});
};
} else {
namespaces[operation.namespace][operation.name] = async (input: unknown) => {
logger.debug(`Executing: ${operationId}`);
return await registry.execute(operationId, input, context);
};
}
}
return namespaces;
}

51
src/error.ts Normal file
View File

@@ -0,0 +1,51 @@
export enum InfrastructureErrorCode {
OPERATION_NOT_FOUND = "OPERATION_NOT_FOUND",
ACCESS_DENIED = "ACCESS_DENIED",
VALIDATION_ERROR = "VALIDATION_ERROR",
TIMEOUT = "TIMEOUT",
ABORTED = "ABORTED",
EXECUTION_ERROR = "EXECUTION_ERROR",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
export type CallErrorCode = InfrastructureErrorCode | string;
export class CallError extends Error {
readonly code: CallErrorCode;
readonly details?: unknown;
constructor(code: CallErrorCode, message: string, details?: unknown) {
super(message);
this.name = "CallError";
this.code = code;
this.details = details;
}
}
export function mapError(
error: unknown,
errorSchemas?: { code: string; schema: unknown }[],
): CallError {
if (error instanceof CallError) {
return error;
}
if (error instanceof Error) {
if (errorSchemas) {
const message = error.message;
for (const schema of errorSchemas) {
if (message.includes(schema.code)) {
return new CallError(schema.code, message, error);
}
}
}
return new CallError(InfrastructureErrorCode.EXECUTION_ERROR, error.message, error);
}
return new CallError(
InfrastructureErrorCode.UNKNOWN_ERROR,
String(error),
{ raw: String(error) },
);
}

151
src/from_mcp.ts Normal file
View File

@@ -0,0 +1,151 @@
import type { IOperationDefinition } from "./types.js";
import { OperationType } from "./types.js";
import { Type, type TSchema } from "@alkdev/typebox";
import { FromSchema } from "./from_schema.js";
import { getLogger } from "@logtape/logtape";
const logger = getLogger("operations:mcp");
export interface MCPClientConfig {
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
headers?: Record<string, string>;
}
export interface MCPClientWrapper {
name: string;
client: unknown;
tools: IOperationDefinition[];
}
export async function createMCPClient(
name: string,
config: MCPClientConfig,
): Promise<MCPClientWrapper> {
logger.info(`Creating MCP client for: ${name}`);
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
const client = new Client({ name: `alkdev-${name}`, version: "1.0.0" });
let transport: any;
if (config.url) {
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
const url = new URL(config.url);
transport = new StreamableHTTPClientTransport(url, {
requestInit: config.headers ? { headers: config.headers } : undefined,
});
} else if (config.command) {
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
transport = new StdioClientTransport({
command: config.command,
args: config.args || [],
env: config.env as Record<string, string> | undefined,
cwd: config.cwd,
});
} else {
throw new Error(`Invalid MCP server config for ${name}: must have either 'url' or 'command'`);
}
await client.connect(transport);
logger.info(`Connected to MCP server: ${name}`);
const toolsResult = await client.listTools();
const operations: IOperationDefinition[] = toolsResult.tools.map((tool: { name: string; description?: string; inputSchema: unknown }) => {
return {
name: tool.name,
namespace: name,
version: "1.0.0",
type: OperationType.MUTATION,
description: tool.description || "",
tags: [],
inputSchema: FromSchema(tool.inputSchema) as TSchema,
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
handler: async (input: unknown) => {
logger.debug(`Calling MCP tool: ${name}.${tool.name}`);
const result = await client.callTool({
name: tool.name,
arguments: input as Record<string, unknown>,
});
if (result.isError) {
throw new Error(`MCP tool error: ${JSON.stringify(result.content)}`);
}
return result.content;
},
} satisfies IOperationDefinition;
});
return {
name,
client,
tools: operations,
};
}
export async function closeMCPClient(wrapper: MCPClientWrapper): Promise<void> {
logger.info(`Closing MCP client: ${wrapper.name}`);
const client = wrapper.client as any;
if (client && typeof client.close === "function") {
await client.close();
}
}
export class MCPClientLoader {
private clients: Map<string, MCPClientWrapper> = new Map();
async load(config: Record<string, MCPClientConfig>): Promise<MCPClientWrapper[]> {
logger.info(`Loading ${Object.keys(config).length} MCP servers`);
const wrappers: MCPClientWrapper[] = [];
for (const [name, serverConfig] of Object.entries(config)) {
try {
const wrapper = await createMCPClient(name, serverConfig);
this.clients.set(name, wrapper);
wrappers.push(wrapper);
} catch (error) {
logger.error(`Failed to load MCP server ${name}: ${error}`);
throw error;
}
}
return wrappers;
}
getClient(name: string): MCPClientWrapper | undefined {
return this.clients.get(name);
}
getAllWrappers(): MCPClientWrapper[] {
return Array.from(this.clients.values());
}
getAllOperations(): IOperationDefinition[] {
const allOps: IOperationDefinition[] = [];
for (const wrapper of this.clients.values()) {
for (const op of wrapper.tools) {
allOps.push(op);
}
}
return allOps;
}
async closeAll(): Promise<void> {
logger.info(`Closing ${this.clients.size} MCP clients`);
const closePromises = Array.from(this.clients.values()).map((wrapper) =>
closeMCPClient(wrapper).catch((error) => {
logger.error(`Error closing MCP client ${wrapper.name}: ${error}`);
})
);
await Promise.all(closePromises);
this.clients.clear();
}
}

339
src/from_openapi.ts Normal file
View File

@@ -0,0 +1,339 @@
import * as Type from "@alkdev/typebox";
import { FromSchema } from "./from_schema.js";
import { OperationType, type IOperationDefinition, type OperationHandler, type OperationContext } from "./types.js";
export interface OpenAPIFS {
readFile(path: string): Promise<string>;
}
export interface OpenAPISpec {
openapi?: string;
swagger?: string;
info: { title: string; version: string; description?: string };
paths: Record<string, Record<string, OpenAPIOperation>>;
components?: { schemas?: Record<string, unknown> };
definitions?: Record<string, unknown>;
basePath?: string;
}
export interface OpenAPIOperation {
operationId?: string;
summary?: string;
description?: string;
tags?: string[];
parameters?: OpenAPIParameter[];
requestBody?: {
content?: Record<string, { schema?: unknown }>;
};
responses?: Record<string, { content?: Record<string, { schema?: unknown }>; description?: string }>;
}
export interface OpenAPIParameter {
name: string;
in: "path" | "query" | "header" | "cookie";
required?: boolean;
schema?: unknown;
description?: string;
}
export interface HTTPServiceConfig {
namespace: string;
baseUrl: string;
headers?: Record<string, string>;
auth?: {
type: "bearer" | "apiKey" | "basic";
token?: string;
headerName?: string;
prefix?: string;
};
timeout?: number;
}
function resolveRef(spec: OpenAPISpec, ref: string): unknown {
if (!ref.startsWith("#/")) {
throw new Error(`External refs not supported: ${ref}`);
}
const parts = ref.slice(2).split("/");
let current: unknown = spec;
for (const part of parts) {
if (typeof current !== "object" || current === null) {
throw new Error(`Cannot resolve ref: ${ref}`);
}
current = (current as Record<string, unknown>)[part];
}
return current;
}
function resolveRefsRecursive(
spec: OpenAPISpec,
schema: unknown,
visited: Set<unknown> = new Set(),
): unknown {
if (typeof schema !== "object" || schema === null) {
return schema;
}
if (visited.has(schema)) {
return { type: "object", description: "[circular reference]" };
}
visited.add(schema);
if (Array.isArray(schema)) {
return schema.map((item) => resolveRefsRecursive(spec, item, visited));
}
const obj = schema as Record<string, unknown>;
if (obj.$ref && typeof obj.$ref === "string") {
const resolved = resolveRef(spec, obj.$ref);
return resolveRefsRecursive(spec, resolved, visited);
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = resolveRefsRecursive(spec, value, visited);
}
return result;
}
function buildInputSchema(
spec: OpenAPISpec,
operation: OpenAPIOperation,
): Type.TSchema {
const properties: Record<string, Type.TSchema> = {};
const required: string[] = [];
if (operation.parameters) {
for (const param of operation.parameters) {
const paramSchema = param.schema
? FromSchema(resolveRefsRecursive(spec, param.schema) as Record<string, unknown>)
: Type.String();
properties[param.name] = paramSchema;
if (param.required) {
required.push(param.name);
}
}
}
if (operation.requestBody?.content?.["application/json"]?.schema) {
const bodySchema = resolveRefsRecursive(
spec,
operation.requestBody.content["application/json"].schema,
) as Record<string, unknown>;
properties.body = FromSchema(bodySchema);
required.push("body");
}
if (Object.keys(properties).length === 0) {
return Type.Object({});
}
const propsWithOptional: Record<string, Type.TSchema> = {};
for (const [key, schema] of Object.entries(properties)) {
if (required.includes(key)) {
propsWithOptional[key] = schema;
} else {
propsWithOptional[key] = Type.Optional(schema);
}
}
return Type.Object(propsWithOptional);
}
function buildOutputSchema(
spec: OpenAPISpec,
operation: OpenAPIOperation,
): Type.TSchema {
const successResponse = operation.responses?.["200"] || operation.responses?.["201"];
if (!successResponse?.content) {
return Type.Unknown();
}
const jsonSchema = successResponse.content["application/json"]?.schema;
if (!jsonSchema) {
const eventStreamSchema = successResponse.content["text/event-stream"]?.schema;
if (eventStreamSchema) {
return FromSchema(resolveRefsRecursive(spec, eventStreamSchema) as Record<string, unknown>);
}
return Type.Unknown();
}
return FromSchema(resolveRefsRecursive(spec, jsonSchema) as Record<string, unknown>);
}
function detectOperationType(method: string, operation: OpenAPIOperation): OperationType {
const successResponse = operation.responses?.["200"] || operation.responses?.["201"];
if (successResponse?.content && "text/event-stream" in successResponse.content) {
return OperationType.SUBSCRIPTION;
}
if (method.toLowerCase() === "get") {
return OperationType.QUERY;
}
return OperationType.MUTATION;
}
function normalizeOperationId(op: OpenAPIOperation, method: string, path: string): string {
if (op.operationId) {
return op.operationId;
}
const pathParts = path.split("/").filter((p) => p && !p.startsWith("{"));
const baseName = pathParts.join("_") || "root";
return `${method}_${baseName}`;
}
function getAuthHeaders(config: HTTPServiceConfig): Record<string, string> {
const headers: Record<string, string> = { ...config.headers };
if (config.auth) {
const token = config.auth.token;
if (token) {
switch (config.auth.type) {
case "bearer":
headers["Authorization"] = `Bearer ${token}`;
break;
case "apiKey":
const headerName = config.auth.headerName || "X-API-Key";
const prefix = config.auth.prefix || "";
headers[headerName] = prefix + token;
break;
case "basic":
headers["Authorization"] = `Basic ${token}`;
break;
}
}
}
return headers;
}
function createHTTPOperation(
spec: OpenAPISpec,
operation: OpenAPIOperation,
method: string,
path: string,
config: HTTPServiceConfig,
): IOperationDefinition {
const operationId = normalizeOperationId(operation, method, path);
const opType = detectOperationType(method, operation);
const authHeaders = getAuthHeaders(config);
const handler: OperationHandler<unknown, unknown, OperationContext> = async (input: unknown, context: OperationContext) => {
const inputObj = (input as Record<string, unknown>) || {};
let urlPath = path;
const queryParams: Record<string, string> = {};
let body: unknown = undefined;
for (const [key, value] of Object.entries(inputObj)) {
if (path.includes(`{${key}}`)) {
urlPath = urlPath.replace(`{${key}}`, encodeURIComponent(String(value)));
} else if (key === "body") {
body = value;
} else {
queryParams[key] = String(value);
}
}
const url = new URL(config.baseUrl + urlPath);
for (const [key, value] of Object.entries(queryParams)) {
url.searchParams.set(key, value);
}
const headers: Record<string, string> = {
...authHeaders,
"Content-Type": "application/json",
};
const response = await fetch(url.toString(), {
method: method.toUpperCase(),
headers,
body: body ? JSON.stringify(body) : undefined,
signal: config.timeout ? AbortSignal.timeout(config.timeout) : undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
return response.json();
} else if (contentType.includes("text/")) {
return response.text();
} else {
return response.arrayBuffer();
}
};
return {
name: operationId,
namespace: config.namespace,
version: "1.0.0",
type: opType,
description: operation.description || operation.summary || `${method.toUpperCase()} ${path}`,
tags: operation.tags,
inputSchema: buildInputSchema(spec, operation),
outputSchema: buildOutputSchema(spec, operation),
accessControl: { requiredScopes: [] },
handler,
_meta: {
method: method.toUpperCase(),
path,
summary: operation.summary,
},
};
}
export function FromOpenAPI(spec: OpenAPISpec, config: HTTPServiceConfig): IOperationDefinition[] {
const operations: IOperationDefinition[] = [];
const basePath = spec.basePath || "";
for (const [path, methods] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(methods)) {
if (!["get", "post", "put", "patch", "delete"].includes(method)) {
continue;
}
if (!operation || typeof operation !== "object") {
continue;
}
const op = operation as OpenAPIOperation;
operations.push(createHTTPOperation(spec, op, method, basePath + path, config));
}
}
return operations;
}
export async function FromOpenAPIFile(path: string, config: HTTPServiceConfig, fs?: OpenAPIFS): Promise<IOperationDefinition[]> {
let content: string;
if (fs) {
content = await fs.readFile(path);
} else {
const { readFile } = await import("node:fs/promises");
content = await readFile(path, "utf-8");
}
const spec = JSON.parse(content) as OpenAPISpec;
return FromOpenAPI(spec, config);
}
export async function FromOpenAPIUrl(url: string, config: HTTPServiceConfig): Promise<IOperationDefinition[]> {
const response = await fetch(url);
const spec = await response.json() as OpenAPISpec;
return FromOpenAPI(spec, config);
}

115
src/from_schema.ts Normal file
View File

@@ -0,0 +1,115 @@
import * as Type from "@alkdev/typebox";
const IsExact = (value: unknown, expect: unknown) => value === expect;
const IsSValue = (value: unknown): value is SValue =>
Type.ValueGuard.IsString(value) || Type.ValueGuard.IsNumber(value) || Type.ValueGuard.IsBoolean(value);
const IsSEnum = (value: unknown): value is SEnum =>
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.enum) && value.enum.every((v) => IsSValue(v));
const IsSAllOf = (value: unknown): value is SAllOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf);
const IsSAnyOf = (value: unknown): value is SAnyOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf);
const IsSOneOf = (value: unknown): value is SOneOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf);
const IsSTuple = (value: unknown): value is STuple =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "array") && Type.ValueGuard.IsArray(value.items);
const IsSArray = (value: unknown): value is SArray =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "array") && !Type.ValueGuard.IsArray(value.items) && Type.ValueGuard.IsObject(value.items);
const IsSConst = (value: unknown): value is SConst => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]);
const IsSString = (value: unknown): value is SString => Type.ValueGuard.IsObject(value) && IsExact(value.type, "string");
const IsSRef = (value: unknown): value is SRef => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsString(value.$ref);
const IsSNumber = (value: unknown): value is SNumber => Type.ValueGuard.IsObject(value) && IsExact(value.type, "number");
const IsSInteger = (value: unknown): value is SInteger => Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer");
const IsSBoolean = (value: unknown): value is SBoolean => Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean");
const IsSNull = (value: unknown): value is SNull => Type.ValueGuard.IsObject(value) && IsExact(value.type, "null");
const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value);
const IsSObject = (value: unknown): value is SObject =>
Type.ValueGuard.IsObject(value) &&
IsExact(value.type, "object") &&
IsSProperties(value.properties) &&
(value.required === undefined || (Type.ValueGuard.IsArray(value.required) && value.required.every((v: unknown) => Type.ValueGuard.IsString(v))));
type SValue = string | number | boolean;
type SEnum = Readonly<{ enum: readonly SValue[] }>;
type SAllOf = Readonly<{ allOf: readonly unknown[] }>;
type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>;
type SOneOf = Readonly<{ oneOf: readonly unknown[] }>;
type SProperties = Record<PropertyKey, unknown>;
type SObject = Readonly<{ type: "object"; properties: SProperties; required?: readonly string[] }>;
type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>;
type SArray = Readonly<{ type: "array"; items: unknown }>;
type SConst = Readonly<{ const: SValue }>;
type SRef = Readonly<{ $ref: string }>;
type SString = Readonly<{ type: "string" }>;
type SNumber = Readonly<{ type: "number" }>;
type SInteger = Readonly<{ type: "integer" }>;
type SBoolean = Readonly<{ type: "boolean" }>;
type SNull = Readonly<{ type: "null" }>;
function FromRest<T extends readonly unknown[]>(T: T): Type.TSchema[] {
return T.map((L) => FromSchema(L)) as never;
}
function FromEnumRest<T extends readonly SValue[]>(T: T): Type.TSchema[] {
return T.map((L) => Type.Literal(L)) as never;
}
function FromAllOf<T extends SAllOf>(T: T): Type.TSchema {
return Type.IntersectEvaluated(FromRest(T.allOf), T);
}
function FromAnyOf<T extends SAnyOf>(T: T): Type.TSchema {
return Type.UnionEvaluated(FromRest(T.anyOf), T);
}
function FromOneOf<T extends SOneOf>(T: T): Type.TSchema {
return Type.UnionEvaluated(FromRest(T.oneOf), T);
}
function FromEnum<T extends SEnum>(T: T): Type.TSchema {
return Type.UnionEvaluated(FromEnumRest(T.enum));
}
function FromTuple<T extends STuple>(T: T): Type.TSchema {
return Type.Tuple(FromRest(T.items), T) as never;
}
function FromArray<T extends SArray>(T: T): Type.TSchema {
return Type.Array(FromSchema(T.items), T) as never;
}
function FromConst<T extends SConst>(T: T): Type.TSchema {
return Type.Literal(T.const, T);
}
function FromRef<T extends SRef>(T: T): Type.TSchema {
return Type.Ref(T.$ref);
}
function FromObject<T extends SObject>(T: T): Type.TSchema {
const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce(
(Acc, K) => {
return {
...Acc,
[K]: T.required && T.required.includes(K) ? FromSchema(T.properties[K]) : Type.Optional(FromSchema(T.properties[K])),
};
},
{} as Type.TProperties,
);
return Type.Object(properties, T) as never;
}
export function FromSchema<T>(T: T): Type.TSchema {
if (IsSAllOf(T)) return FromAllOf(T);
if (IsSAnyOf(T)) return FromAnyOf(T);
if (IsSOneOf(T)) return FromOneOf(T);
if (IsSEnum(T)) return FromEnum(T);
if (IsSObject(T)) return FromObject(T);
if (IsSTuple(T)) return FromTuple(T);
if (IsSArray(T)) return FromArray(T);
if (IsSConst(T)) return FromConst(T);
if (IsSRef(T)) return FromRef(T);
if (IsSString(T)) return Type.String(T);
if (IsSNumber(T)) return Type.Number(T);
if (IsSInteger(T)) return Type.Integer(T);
if (IsSBoolean(T)) return Type.Boolean(T);
if (IsSNull(T)) return Type.Null(T);
return Type.Unknown(T || {});
}

18
src/index.ts Normal file
View File

@@ -0,0 +1,18 @@
export { OperationType, OperationContextSchema, OperationDefinitionSchema, OperationSpecSchema, AccessControlSchema, ErrorDefinitionSchema } from "./types.js";
export type { IOperationDefinition, OperationHandler, SubscriptionHandler, Identity, OperationEnv, OperationContext, OperationSpec, AccessControl, ErrorDefinition } from "./types.js";
export { OperationRegistry } from "./registry.js";
export { formatValueErrors, assertIsSchema, validateOrThrow, collectErrors } from "./validation.js";
export { buildEnv } from "./env.js";
export type { PendingRequestMap, EnvOptions } from "./env.js";
export { FromSchema } from "./from_schema.js";
export { FromOpenAPI, FromOpenAPIFile, FromOpenAPIUrl } from "./from_openapi.js";
export type { OpenAPISpec, OpenAPIOperation, OpenAPIParameter, HTTPServiceConfig, OpenAPIFS } from "./from_openapi.js";
export { scanOperations } from "./scanner.js";
export type { OperationManifest, ScannerFS } from "./scanner.js";
export { CallError, InfrastructureErrorCode, mapError } from "./error.js";
export type { CallErrorCode } from "./error.js";
export { PendingRequestMap as PendingRequestMapClass, 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";

78
src/registry.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { IOperationDefinition, OperationContext, OperationSpec } from "./types.js";
import { getLogger } from "@logtape/logtape";
import { Value } from "@alkdev/typebox/value";
import { assertIsSchema, validateOrThrow, collectErrors, formatValueErrors } from "./validation.js";
const logger = getLogger("operations:registry");
export class OperationRegistry {
private operations = new Map<string, IOperationDefinition>();
private getOperationId(operation: IOperationDefinition): string {
return `${operation.namespace}.${operation.name}`;
}
register(operation: IOperationDefinition): void {
const opId = `${operation.namespace}.${operation.name}`;
assertIsSchema(operation.inputSchema, `${opId} inputSchema`);
assertIsSchema(operation.outputSchema, `${opId} outputSchema`);
const id = this.getOperationId(operation);
this.operations.set(id, operation);
logger.info(`Registered operation: ${id}`);
}
registerAll(operations: IOperationDefinition[]): void {
for (const op of operations) {
this.register(op);
}
}
get(id: string): IOperationDefinition | undefined {
return this.operations.get(id);
}
getByName(namespace: string, name: string): IOperationDefinition | undefined {
return this.operations.get(`${namespace}.${name}`);
}
list(): IOperationDefinition[] {
return Array.from(this.operations.values());
}
private extractSpec(operation: IOperationDefinition): OperationSpec {
const { handler: _handler, ...spec } = operation;
return spec;
}
getSpec(id: string): OperationSpec | undefined {
const operation = this.operations.get(id);
return operation ? this.extractSpec(operation) : undefined;
}
getAllSpecs(): OperationSpec[] {
return this.list().map(op => this.extractSpec(op));
}
async execute<TInput = unknown, TOutput = unknown>(
operationId: string,
input: TInput,
context: OperationContext,
): Promise<TOutput> {
const operation = this.operations.get(operationId);
if (!operation) {
throw new Error(`Operation not found: ${operationId}`);
}
validateOrThrow(operation.inputSchema, input, `Input validation failed for ${operationId}`);
const result = await operation.handler(input, context) as TOutput;
const errors = collectErrors(operation.outputSchema, result);
if (errors.length > 0) {
logger.warn(`Output validation failed for ${operationId}:\n${formatValueErrors(errors)}`);
}
return result;
}
}

97
src/scanner.ts Normal file
View File

@@ -0,0 +1,97 @@
import type { IOperationDefinition } from "./types.js";
import { OperationDefinitionSchema } from "./types.js";
import { collectErrors, formatValueErrors } from "./validation.js";
import { getLogger } from "@logtape/logtape";
const logger = getLogger("operations:scanner");
export interface ScannerFS {
readdir(path: string): AsyncIterable<{ name: string; isFile: boolean; isDirectory: boolean }>;
cwd(): string;
}
export interface OperationManifest {
operations: Record<string, IOperationDefinition>;
baseUrl?: string;
}
export async function scanOperations(
dirPath: string,
fs: ScannerFS,
): Promise<IOperationDefinition[]> {
const operations: IOperationDefinition[] = [];
try {
await processDirectory(dirPath, operations, fs);
} catch (error) {
logger.error(
`Error scanning directory ${dirPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
throw error;
}
return operations;
}
async function processDirectory(
dirPath: string,
operations: IOperationDefinition[],
fs: ScannerFS,
): Promise<void> {
try {
for await (const entry of fs.readdir(dirPath)) {
const fullPath = `${dirPath}/${entry.name}`;
if (entry.isDirectory) {
await processDirectory(fullPath, operations, fs);
} else if (entry.isFile && entry.name.endsWith(".ts")) {
try {
const absolutePath = fullPath.startsWith("/") ? fullPath : `${fs.cwd()}/${fullPath}`;
const moduleUrl = pathToFileURL(absolutePath);
const module = await import(moduleUrl);
if (module.default) {
const operation = module.default as IOperationDefinition;
const errors = collectErrors(OperationDefinitionSchema, operation);
if (errors.length > 0) {
logger.warn(`${fullPath}: Invalid operation definition - ${formatValueErrors(errors, "")}`);
continue;
}
operations.push(operation);
logger.info(
`Loaded operation: ${operation.namespace}.${operation.name} from ${fullPath}`,
);
} else {
logger.warn(`${fullPath} does not export a default operation`);
}
} catch (error) {
logger.error(
`Error processing ${fullPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
}
} catch (error) {
logger.error(
`Error reading directory ${dirPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
throw new Error(
`Failed to process directory ${dirPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
function pathToFileURL(absolutePath: string): string {
return `file://${absolutePath}`;
}

28
src/subscribe.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { IOperationDefinition, OperationContext } from "./types.js";
import { OperationRegistry } from "./registry.js";
export async function* subscribe(
registry: OperationRegistry,
operationId: string,
input: unknown,
context: OperationContext,
): AsyncGenerator<unknown, void, unknown> {
const operation = registry.get(operationId);
if (!operation) {
throw new Error(`Operation not found: ${operationId}`);
}
const handler = operation.handler;
const generator = handler(input, context) as AsyncGenerator<unknown, void, unknown>;
try {
for await (const value of generator) {
yield value;
}
} finally {
if (generator.return) {
await generator.return(undefined);
}
}
}

144
src/types.ts Normal file
View File

@@ -0,0 +1,144 @@
import { Type, type Static, type TSchema } from "@alkdev/typebox";
export enum OperationType {
QUERY = "query",
MUTATION = "mutation",
SUBSCRIPTION = "subscription",
}
export interface Identity {
id: string
scopes: string[]
resources?: Record<string, string[]>
}
export type OperationEnv = Record<string, Record<string, (input: unknown) => Promise<unknown>>>
export const OperationContextSchema = Type.Object({
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
requestId: Type.Optional(Type.String()),
parentRequestId: Type.Optional(Type.String()),
identity: Type.Optional(Type.Object({
id: Type.String(),
scopes: Type.Array(Type.String()),
resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String())))
})),
}, {
description: "Context provided to all operation handlers"
});
type OperationContextBase = Static<typeof OperationContextSchema>
export type OperationContext = OperationContextBase & {
env?: OperationEnv
stream?: () => AsyncIterable<unknown>
pubsub?: unknown
}
export const ErrorDefinitionSchema = Type.Object({
code: Type.String({
description: "Error Code e.g., INVALID_INPUT, NOT_FOUND, UNAUTHORIZED"
}),
description: Type.String(),
schema: Type.Unknown(),
httpStatus: Type.Optional(Type.Number()),
});
export type ErrorDefinition = Static<typeof ErrorDefinitionSchema>;
export const AccessControlSchema = Type.Object({
requiredScopes: Type.Array(
Type.String(),
{description: "Required scopes (all must be present)"}
),
requiredScopesAny: Type.Optional(
Type.Array(Type.String({description: "Required scopes (at least one must match)"}))),
resourceType: Type.Optional(Type.String({description: "Resource Type e.g., project, tool, data"})),
resourceAction: Type.Optional(Type.String({description: "Required action on the resource e.g., read, write, execute"})),
customAuth: Type.Optional(Type.String({description: "Name of custom auth function"})),
});
export type AccessControl = Static<typeof AccessControlSchema>;
export type OperationHandler<
TInput = unknown,
TOutput = unknown,
TContext extends OperationContext = OperationContext,
> = (
input: TInput,
context: TContext,
) => Promise<TOutput> | TOutput;
export type SubscriptionHandler<
TInput = unknown,
TOutput = unknown,
TContext extends OperationContext = OperationContext,
> = (
input: TInput,
context: TContext,
) => AsyncGenerator<TOutput, void, unknown>;
export const OperationDefinitionSchema = Type.Object({
name: Type.String({ description: "Unique operation name" }),
namespace: Type.String({
description: "Namespace for grouping (e.g., 'task', 'graph', 'user')",
}),
version: Type.String({ description: "Semantic version (e.g., '1.0.0')" }),
type: Type.Enum(OperationType, {
description: "Operation type: query, mutation, or subscription",
}),
title: Type.Optional(Type.String({ description: "Human-readable title" })),
description: Type.String({ description: "Detailed description" }),
tags: Type.Optional(Type.Array(Type.String())),
inputSchema: Type.Unknown({ description: "json schema for input" }),
outputSchema: Type.Unknown({ description: "json schema for output" }),
errorSchemas: Type.Optional(Type.Array(ErrorDefinitionSchema)),
accessControl: AccessControlSchema,
handler: Type.Unknown({ description: "Operation handler function" }),
_meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
});
export interface OperationSpec<
TInput = unknown,
TOutput = unknown,
> {
name: string;
namespace: string;
version: string;
type: OperationType;
title?: string;
description: string;
tags?: string[];
inputSchema: TSchema;
outputSchema: TSchema;
errorSchemas?: ErrorDefinition[];
accessControl: AccessControl;
_meta?: Record<string, unknown>;
}
export const OperationSpecSchema = Type.Object({
name: Type.String({ description: "Unique operation name" }),
namespace: Type.String({
description: "Namespace for grouping (e.g., 'task', 'graph', 'user')",
}),
version: Type.String({ description: "Semantic version (e.g., '1.0.0')" }),
type: Type.Enum(OperationType, {
description: "Operation type: query, mutation, or subscription",
}),
title: Type.Optional(Type.String({ description: "Human-readable title" })),
description: Type.String({ description: "Detailed description" }),
tags: Type.Optional(Type.Array(Type.String())),
inputSchema: Type.Unknown({ description: "json schema for input" }),
outputSchema: Type.Unknown({ description: "json schema for output" }),
errorSchemas: Type.Optional(Type.Array(ErrorDefinitionSchema)),
accessControl: AccessControlSchema,
_meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
});
export interface IOperationDefinition<
TInput = unknown,
TOutput = unknown,
TContext extends OperationContext = OperationContext,
> extends OperationSpec<TInput, TOutput> {
handler: OperationHandler<TInput, TOutput, TContext> | SubscriptionHandler<TInput, TOutput, TContext>;
}

44
src/validation.ts Normal file
View File

@@ -0,0 +1,44 @@
import { KindGuard, type TSchema } from "@alkdev/typebox";
import { Value } from "@alkdev/typebox/value";
export function formatValueErrors(
errors: Iterable<{ path: string; message: string }>,
indent: string = " - ",
): string {
return [...errors]
.map((err) => `${indent}${err.path}: ${err.message}`)
.join("\n");
}
export function assertIsSchema(schema: unknown, context?: string): void {
const contextMsg = context ? ` for ${context}` : "";
if (!KindGuard.IsSchema(schema)) {
throw new Error(`Not a valid TypeBox schema${contextMsg}. Use FromSchema() to convert JSON Schema to TypeBox.`);
}
}
export function validateOrThrow(
schema: TSchema,
value: unknown,
context?: string,
): void {
if (!Value.Check(schema, value)) {
const errors = Value.Errors(schema, value);
const formatted = formatValueErrors(errors);
const contextMsg = context ? ` for ${context}` : "";
throw new Error(`Validation failed${contextMsg}:\n${formatted}`);
}
}
export function collectErrors(
schema: TSchema,
value: unknown,
): Array<{ path: string; message: string }> {
if (Value.Check(schema, value)) {
return [];
}
return [...Value.Errors(schema, value)].map((err) => ({
path: err.path,
message: err.message,
}));
}