feat: add SchemaAdapter system for typemap integration (Zod/Valibot), fix scanner pathToFileURL
- Add from_typemap.ts with SchemaAdapter interface, defaultAdapter, zodAdapter(), valibotAdapter()
- OperationRegistry now accepts optional { schemaAdapter } config; default is TypeBox passthrough
- Adapters use @alkdev/typemap (optional peer dep) with lazy init; detect schemas via ~standard vendor protocol
- Fix scanner pathToFileURL to encode URI components and normalize Windows backslashes
- Add from-typemap sub-path export in package.json and tsup config
- Add test suite covering defaultAdapter, zodAdapter, valibotAdapter, and registry integration
This commit is contained in:
44
package-lock.json
generated
44
package-lock.json
generated
@@ -14,20 +14,26 @@
|
||||
"@logtape/logtape": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alkdev/typemap": "^0.10.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.7.0",
|
||||
"valibot": "^1.4.0",
|
||||
"vitest": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@alkdev/typemap": "^0.10.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@alkdev/typemap": {
|
||||
"optional": true
|
||||
},
|
||||
"@modelcontextprotocol/sdk": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -36,9 +42,6 @@
|
||||
"../pubsub": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@repeaterjs/repeater": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
@@ -205,10 +208,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"../pubsub/node_modules/@repeaterjs/repeater": {
|
||||
"version": "3.0.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../pubsub/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"cpu": [
|
||||
@@ -1928,6 +1927,18 @@
|
||||
"version": "0.34.49",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@alkdev/typemap": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@alkdev/typemap/-/typemap-0.10.1.tgz",
|
||||
"integrity": "sha512-nCrZrrxgyWr3xBXjCWhUj56rH/Yb64l96Yl8gr3fYF5JS+nfiTgLcJ0shRvSAA8nznsiPfMJgmNrTGsdyHYs2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@alkdev/typebox": "^0.34.49",
|
||||
"valibot": "^1.0.0",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
@@ -5163,6 +5174,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/valibot": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.0.tgz",
|
||||
"integrity": "sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"dev": true,
|
||||
@@ -5455,7 +5481,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.1",
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
|
||||
22
package.json
22
package.json
@@ -26,6 +26,16 @@
|
||||
"types": "./dist/from-mcp.d.cts",
|
||||
"default": "./dist/from-mcp.cjs"
|
||||
}
|
||||
},
|
||||
"./from-typemap": {
|
||||
"import": {
|
||||
"types": "./dist/from-typemap.d.ts",
|
||||
"default": "./dist/from-typemap.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/from-typemap.d.cts",
|
||||
"default": "./dist/from-typemap.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -53,27 +63,33 @@
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@alkdev/typebox": "^0.34.49",
|
||||
"@alkdev/pubsub": "^0.1.0",
|
||||
"@alkdev/typebox": "^0.34.49",
|
||||
"@logtape/logtape": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@alkdev/typemap": "^0.10.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@modelcontextprotocol/sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@alkdev/typemap": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@alkdev/typemap": "^0.10.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.7.0",
|
||||
"valibot": "^1.4.0",
|
||||
"vitest": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
src/from_typemap.ts
Normal file
115
src/from_typemap.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { TSchema } from "@alkdev/typebox";
|
||||
import { KindGuard } from "@alkdev/typebox";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
|
||||
const logger = getLogger("operations:typemap");
|
||||
|
||||
export interface SchemaAdapter {
|
||||
toTypeBox(schema: unknown): TSchema;
|
||||
init?(): Promise<void>;
|
||||
}
|
||||
|
||||
export const defaultAdapter: SchemaAdapter = {
|
||||
toTypeBox(schema: unknown): TSchema {
|
||||
if (KindGuard.IsSchema(schema)) {
|
||||
return schema as TSchema;
|
||||
}
|
||||
throw new Error(
|
||||
`SchemaAdapter: expected a TypeBox schema, but received ${typeof schema}. ` +
|
||||
`Install @alkdev/typemap and use zodAdapter/valibotAdapter to convert from other schema libraries.`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function isStandardSchemaFrom(value: unknown, vendor: string): boolean {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const standard = (value as Record<string, unknown>)["~standard"];
|
||||
if (typeof standard !== "object" || standard === null) return false;
|
||||
return (standard as Record<string, unknown>)["vendor"] === vendor;
|
||||
}
|
||||
|
||||
export function zodAdapter(): SchemaAdapter & { init(): Promise<void> } {
|
||||
let TypeBoxFromZod: ((schema: unknown) => TSchema) | null = null;
|
||||
let loaded = false;
|
||||
|
||||
return {
|
||||
async init(): Promise<void> {
|
||||
if (loaded) return;
|
||||
try {
|
||||
const typemap = await import("@alkdev/typemap");
|
||||
TypeBoxFromZod = typemap.TypeBoxFromZod as ((schema: unknown) => TSchema);
|
||||
loaded = true;
|
||||
logger.info("zodAdapter: loaded @alkdev/typemap");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"zodAdapter requires @alkdev/typemap as a peer dependency. " +
|
||||
"Install it with: npm install @alkdev/typemap"
|
||||
);
|
||||
}
|
||||
},
|
||||
toTypeBox(schema: unknown): TSchema {
|
||||
if (KindGuard.IsSchema(schema)) {
|
||||
return schema as TSchema;
|
||||
}
|
||||
if (isStandardSchemaFrom(schema, "zod") && TypeBoxFromZod) {
|
||||
return TypeBoxFromZod(schema);
|
||||
}
|
||||
if (TypeBoxFromZod && !isStandardSchemaFrom(schema, "zod") && !isStandardSchemaFrom(schema, "valibot")) {
|
||||
throw new Error(
|
||||
`zodAdapter: schema is not a Zod or TypeBox schema (received ${typeof schema})`
|
||||
);
|
||||
}
|
||||
if (TypeBoxFromZod) {
|
||||
throw new Error(
|
||||
`zodAdapter: schema uses a different schema library than Zod. Use the appropriate adapter.`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
"zodAdapter: not initialized. Call await adapter.init() before using toTypeBox()."
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function valibotAdapter(): SchemaAdapter & { init(): Promise<void> } {
|
||||
let TypeBoxFromValibot: ((schema: unknown) => TSchema) | null = null;
|
||||
let loaded = false;
|
||||
|
||||
return {
|
||||
async init(): Promise<void> {
|
||||
if (loaded) return;
|
||||
try {
|
||||
const typemap = await import("@alkdev/typemap");
|
||||
TypeBoxFromValibot = typemap.TypeBoxFromValibot as ((schema: unknown) => TSchema);
|
||||
loaded = true;
|
||||
logger.info("valibotAdapter: loaded @alkdev/typemap");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"valibotAdapter requires @alkdev/typemap as a peer dependency. " +
|
||||
"Install it with: npm install @alkdev/typemap"
|
||||
);
|
||||
}
|
||||
},
|
||||
toTypeBox(schema: unknown): TSchema {
|
||||
if (KindGuard.IsSchema(schema)) {
|
||||
return schema as TSchema;
|
||||
}
|
||||
if (isStandardSchemaFrom(schema, "valibot") && TypeBoxFromValibot) {
|
||||
return TypeBoxFromValibot(schema);
|
||||
}
|
||||
if (TypeBoxFromValibot && !isStandardSchemaFrom(schema, "valibot") && !isStandardSchemaFrom(schema, "zod")) {
|
||||
throw new Error(
|
||||
`valibotAdapter: schema is not a Valibot or TypeBox schema (received ${typeof schema})`
|
||||
);
|
||||
}
|
||||
if (TypeBoxFromValibot) {
|
||||
throw new Error(
|
||||
`valibotAdapter: schema uses a different schema library than Valibot. Use the appropriate adapter.`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
"valibotAdapter: not initialized. Call await adapter.init() before using toTypeBox()."
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { OperationType, OperationContextSchema, 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 type { RegistryOptions } from "./registry.js";
|
||||
export { formatValueErrors, assertIsSchema, validateOrThrow, collectErrors } from "./validation.js";
|
||||
export { buildEnv } from "./env.js";
|
||||
export type { EnvOptions } from "./env.js";
|
||||
@@ -15,6 +16,8 @@ export { PendingRequestMap, buildCallHandler } from "./call.js";
|
||||
export type { CallEventMap, CallEventMapValue, CallRequestedEvent, CallRespondedEvent, CallAbortedEvent, CallErrorEvent, CallHandler, CallHandlerConfig } from "./call.js";
|
||||
export { checkAccess } from "./access.js";
|
||||
export { subscribe } from "./subscribe.js";
|
||||
export { defaultAdapter, zodAdapter, valibotAdapter } from "./from_typemap.js";
|
||||
export type { SchemaAdapter } from "./from_typemap.js";
|
||||
export { createMCPClient, closeMCPClient, MCPClientLoader } 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";
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
import type { OperationContext, OperationSpec, OperationHandler, SubscriptionHandler, Identity, AccessControl } from "./types.js";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import { Value } from "@alkdev/typebox/value";
|
||||
import { KindGuard } from "@alkdev/typebox";
|
||||
import { KindGuard, type TSchema } from "@alkdev/typebox";
|
||||
import { assertIsSchema, validateOrThrow, collectErrors, formatValueErrors } from "./validation.js";
|
||||
import { isResponseEnvelope, localEnvelope, type ResponseEnvelope } from "./response-envelope.js";
|
||||
import { CallError, InfrastructureErrorCode } from "./error.js";
|
||||
import { checkAccess } from "./access.js";
|
||||
import type { SchemaAdapter } from "./from_typemap.js";
|
||||
import { defaultAdapter } from "./from_typemap.js";
|
||||
|
||||
const logger = getLogger("operations:registry");
|
||||
|
||||
export interface RegistryOptions {
|
||||
schemaAdapter?: SchemaAdapter;
|
||||
}
|
||||
|
||||
export class OperationRegistry {
|
||||
private specs = new Map<string, OperationSpec>();
|
||||
private handlers = new Map<string, OperationHandler | SubscriptionHandler>();
|
||||
private adapter: SchemaAdapter;
|
||||
|
||||
constructor(options?: RegistryOptions) {
|
||||
this.adapter = options?.schemaAdapter ?? defaultAdapter;
|
||||
}
|
||||
|
||||
private opId(namespace: string, name: string): string {
|
||||
return `${namespace}.${name}`;
|
||||
}
|
||||
|
||||
private toTypeBox(schema: unknown): TSchema {
|
||||
return this.adapter.toTypeBox(schema);
|
||||
}
|
||||
|
||||
register(operation: OperationSpec & { handler?: OperationHandler | SubscriptionHandler }): void {
|
||||
const id = this.opId(operation.namespace, operation.name);
|
||||
assertIsSchema(operation.inputSchema, `${id} inputSchema`);
|
||||
assertIsSchema(operation.outputSchema, `${id} outputSchema`);
|
||||
const inputSchema = this.toTypeBox(operation.inputSchema);
|
||||
const outputSchema = this.toTypeBox(operation.outputSchema);
|
||||
const { handler, ...spec } = operation;
|
||||
this.specs.set(id, spec);
|
||||
const resolvedSpec: OperationSpec = { ...spec, inputSchema, outputSchema };
|
||||
this.specs.set(id, resolvedSpec);
|
||||
if (handler) {
|
||||
this.handlers.set(id, handler);
|
||||
}
|
||||
@@ -37,9 +53,10 @@ export class OperationRegistry {
|
||||
|
||||
registerSpec(spec: OperationSpec): void {
|
||||
const id = this.opId(spec.namespace, spec.name);
|
||||
assertIsSchema(spec.inputSchema, `${id} inputSchema`);
|
||||
assertIsSchema(spec.outputSchema, `${id} outputSchema`);
|
||||
this.specs.set(id, spec);
|
||||
const inputSchema = this.toTypeBox(spec.inputSchema);
|
||||
const outputSchema = this.toTypeBox(spec.outputSchema);
|
||||
const resolvedSpec: OperationSpec = { ...spec, inputSchema, outputSchema };
|
||||
this.specs.set(id, resolvedSpec);
|
||||
logger.info(`Registered spec: ${id}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,5 +93,9 @@ async function processDirectory(
|
||||
}
|
||||
|
||||
function pathToFileURL(absolutePath: string): string {
|
||||
return `file://${absolutePath}`;
|
||||
let normalized = absolutePath.replace(/\\/g, "/");
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
return `file://${encodeURI(normalized).replace(/#/g, "%23")}`;
|
||||
}
|
||||
187
test/from_typemap.test.ts
Normal file
187
test/from_typemap.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { defaultAdapter, zodAdapter, valibotAdapter, type SchemaAdapter } from "../src/from_typemap.js";
|
||||
import * as Type from "@alkdev/typebox";
|
||||
import { KindGuard } from "@alkdev/typebox";
|
||||
import { OperationRegistry, type RegistryOptions } from "../src/registry.js";
|
||||
import { OperationType, type IOperationDefinition } from "../src/index.js";
|
||||
|
||||
describe("defaultAdapter", () => {
|
||||
it("passes through TypeBox schemas unchanged", () => {
|
||||
const schema = Type.Object({ name: Type.String() });
|
||||
const result = defaultAdapter.toTypeBox(schema);
|
||||
expect(result).toBe(schema);
|
||||
});
|
||||
|
||||
it("throws for non-TypeBox schemas", () => {
|
||||
expect(() => defaultAdapter.toTypeBox({})).toThrow("expected a TypeBox schema");
|
||||
expect(() => defaultAdapter.toTypeBox("string")).toThrow("expected a TypeBox schema");
|
||||
expect(() => defaultAdapter.toTypeBox(42)).toThrow("expected a TypeBox schema");
|
||||
});
|
||||
});
|
||||
|
||||
describe("zodAdapter", () => {
|
||||
it("passes through TypeBox schemas without init", () => {
|
||||
const adapter = zodAdapter();
|
||||
const schema = Type.Object({ name: Type.String() });
|
||||
const result = adapter.toTypeBox(schema);
|
||||
expect(result).toBe(schema);
|
||||
});
|
||||
|
||||
it("throws for non-TypeBox schemas when not initialized", () => {
|
||||
const adapter = zodAdapter();
|
||||
expect(() => adapter.toTypeBox({})).toThrow("not initialized");
|
||||
});
|
||||
|
||||
it("converts Zod schemas after init", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
const { z } = await import("zod");
|
||||
const zodSchema = z.object({ name: z.string(), age: z.number() });
|
||||
const result = adapter.toTypeBox(zodSchema);
|
||||
expect(KindGuard.IsSchema(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("passes through TypeBox schemas after init", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
const schema = Type.Object({ name: Type.String() });
|
||||
const result = adapter.toTypeBox(schema);
|
||||
expect(result).toBe(schema);
|
||||
});
|
||||
|
||||
it("throws for unrecognized schemas after init", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
expect(() => adapter.toTypeBox(42)).toThrow("not a Zod or TypeBox schema");
|
||||
});
|
||||
|
||||
it("init is idempotent", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
await adapter.init();
|
||||
});
|
||||
});
|
||||
|
||||
describe("valibotAdapter", () => {
|
||||
it("passes through TypeBox schemas without init", () => {
|
||||
const adapter = valibotAdapter();
|
||||
const schema = Type.Object({ name: Type.String() });
|
||||
const result = adapter.toTypeBox(schema);
|
||||
expect(result).toBe(schema);
|
||||
});
|
||||
|
||||
it("throws for non-TypeBox schemas when not initialized", () => {
|
||||
const adapter = valibotAdapter();
|
||||
expect(() => adapter.toTypeBox({})).toThrow("not initialized");
|
||||
});
|
||||
|
||||
it("converts Valibot schemas after init", async () => {
|
||||
const adapter = valibotAdapter();
|
||||
await adapter.init();
|
||||
const v = await import("valibot");
|
||||
const valibotSchema = v.object({ id: v.string(), active: v.boolean() });
|
||||
const result = adapter.toTypeBox(valibotSchema);
|
||||
expect(KindGuard.IsSchema(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("passes through TypeBox schemas after init", async () => {
|
||||
const adapter = valibotAdapter();
|
||||
await adapter.init();
|
||||
const schema = Type.Object({ name: Type.String() });
|
||||
const result = adapter.toTypeBox(schema);
|
||||
expect(result).toBe(schema);
|
||||
});
|
||||
|
||||
it("throws for unrecognized schemas after init", async () => {
|
||||
const adapter = valibotAdapter();
|
||||
await adapter.init();
|
||||
expect(() => adapter.toTypeBox(42)).toThrow("not a Valibot or TypeBox schema");
|
||||
});
|
||||
|
||||
it("init is idempotent", async () => {
|
||||
const adapter = valibotAdapter();
|
||||
await adapter.init();
|
||||
await adapter.init();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OperationRegistry with SchemaAdapter", () => {
|
||||
function makeOperation(overrides: Partial<IOperationDefinition> = {}): IOperationDefinition {
|
||||
return {
|
||||
name: "testOp",
|
||||
namespace: "test",
|
||||
version: "1.0.0",
|
||||
type: OperationType.QUERY,
|
||||
description: "A test operation",
|
||||
inputSchema: Type.Object({ value: Type.String() }),
|
||||
outputSchema: Type.Object({ result: Type.String() }),
|
||||
accessControl: { requiredScopes: [] },
|
||||
handler: async (input: any) => ({ result: `processed: ${input.value}` }),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it("works with default adapter (TypeBox schemas)", async () => {
|
||||
const registry = new OperationRegistry();
|
||||
registry.register(makeOperation());
|
||||
const result = await registry.execute("test.testOp", { value: "hello" }, {});
|
||||
expect(result.data).toEqual({ result: "processed: hello" });
|
||||
});
|
||||
|
||||
it("works with zodAdapter after init", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
const registry = new OperationRegistry({ schemaAdapter: adapter });
|
||||
const { z } = await import("zod");
|
||||
registry.register({
|
||||
name: "zodOp",
|
||||
namespace: "test",
|
||||
version: "1.0.0",
|
||||
type: OperationType.QUERY,
|
||||
description: "Operation with Zod schemas",
|
||||
inputSchema: z.object({ name: z.string() }) as any,
|
||||
outputSchema: z.object({ greeting: z.string() }) as any,
|
||||
accessControl: { requiredScopes: [] },
|
||||
handler: async (input: any) => ({ greeting: `Hello, ${input.name}` }),
|
||||
});
|
||||
const result = await registry.execute("test.zodOp", { name: "world" }, {});
|
||||
expect(result.data).toEqual({ greeting: "Hello, world" });
|
||||
});
|
||||
|
||||
it("works with valibotAdapter after init", async () => {
|
||||
const adapter = valibotAdapter();
|
||||
await adapter.init();
|
||||
const registry = new OperationRegistry({ schemaAdapter: adapter });
|
||||
const v = await import("valibot");
|
||||
registry.register({
|
||||
name: "valOp",
|
||||
namespace: "test",
|
||||
version: "1.0.0",
|
||||
type: OperationType.QUERY,
|
||||
description: "Operation with Valibot schemas",
|
||||
inputSchema: v.object({ id: v.string() }) as any,
|
||||
outputSchema: v.object({ status: v.string() }) as any,
|
||||
accessControl: { requiredScopes: [] },
|
||||
handler: async (input: any) => ({ status: `ok: ${input.id}` }),
|
||||
});
|
||||
const result = await registry.execute("test.valOp", { id: "abc" }, {});
|
||||
expect(result.data).toEqual({ status: "ok: abc" });
|
||||
});
|
||||
|
||||
it("throws with defaultAdapter for non-TypeBox schemas", () => {
|
||||
const registry = new OperationRegistry();
|
||||
expect(() => registry.register({
|
||||
...makeOperation(),
|
||||
inputSchema: {} as any,
|
||||
})).toThrow("expected a TypeBox schema");
|
||||
});
|
||||
|
||||
it("can use TypeBox schemas with zodAdapter (mixed)", async () => {
|
||||
const adapter = zodAdapter();
|
||||
await adapter.init();
|
||||
const registry = new OperationRegistry({ schemaAdapter: adapter });
|
||||
registry.register(makeOperation());
|
||||
const result = await registry.execute("test.testOp", { value: "hello" }, {});
|
||||
expect(result.data).toEqual({ result: "processed: hello" });
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ export default defineConfig({
|
||||
entry: [
|
||||
'src/index.ts',
|
||||
'src/from_mcp.ts',
|
||||
'src/from_typemap.ts',
|
||||
],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
|
||||
Reference in New Issue
Block a user