From ffbb61d58acc83d5c37801a9489f6f4ddf1a726a Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 2 Aug 2025 16:33:05 +0200 Subject: [PATCH] initialized mcp support --- app/__test__/app/mcp.spec.ts | 42 +++++++ app/package.json | 2 +- app/src/auth/auth-schema.ts | 6 +- app/src/auth/authenticate/Authenticator.ts | 54 ++++---- app/src/cli/commands/index.ts | 1 + app/src/cli/commands/mcp/mcp.ts | 97 +++++++++++++++ app/src/core/utils/index.ts | 16 +-- app/src/data/AppData.ts | 10 +- app/src/data/api/DataController.ts | 31 +++++ app/src/media/media-schema.ts | 4 +- app/src/modules/Controller.ts | 2 + app/src/modules/Module.ts | 2 + app/src/modules/ModuleManager.ts | 8 +- app/src/modules/mcp/$object.ts | 136 +++++++++++++++++++++ app/src/modules/mcp/index.ts | 1 + app/src/modules/mcp/utils.spec.ts | 36 ++++++ app/src/modules/mcp/utils.ts | 51 ++++++++ app/src/modules/server/AppServer.ts | 31 +++-- bun.lock | 6 +- 19 files changed, 468 insertions(+), 68 deletions(-) create mode 100644 app/__test__/app/mcp.spec.ts create mode 100644 app/src/cli/commands/mcp/mcp.ts create mode 100644 app/src/modules/mcp/$object.ts create mode 100644 app/src/modules/mcp/index.ts create mode 100644 app/src/modules/mcp/utils.spec.ts create mode 100644 app/src/modules/mcp/utils.ts diff --git a/app/__test__/app/mcp.spec.ts b/app/__test__/app/mcp.spec.ts new file mode 100644 index 0000000..15c19ea --- /dev/null +++ b/app/__test__/app/mcp.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "bun:test"; +import { makeAppFromEnv } from "cli/commands/run"; +import { createApp } from "core/test/utils"; +import { ObjectToolSchema } from "modules/mcp"; +import { s } from "bknd/utils"; + +describe("mcp", () => { + it("...", async () => { + const app = createApp({ + initialConfig: { + auth: { + enabled: true, + }, + }, + }); + await app.build(); + + const appConfig = app.modules.configs(); + const { version, ...appSchema } = app.getSchema(); + + const schema = s.strictObject(appSchema); + + const nodes = [...schema.walk({ data: appConfig })] + .map((n) => { + const path = n.instancePath.join("."); + if (path.startsWith("auth")) { + console.log("schema", n.instancePath, n.schema.constructor.name); + if (path === "auth.jwt") { + //console.log("jwt", n.schema.IS_MCP); + } + } + return n; + }) + .filter((n) => n.schema instanceof ObjectToolSchema) as s.Node[]; + const tools = nodes.flatMap((n) => n.schema.getTools(n)); + + console.log( + "tools", + tools.map((t) => t.name), + ); + }); +}); diff --git a/app/package.json b/app/package.json index aec8170..33d7f27 100644 --- a/app/package.json +++ b/app/package.json @@ -100,7 +100,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.3.2", + "jsonv-ts": "^0.5.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", diff --git a/app/src/auth/auth-schema.ts b/app/src/auth/auth-schema.ts index aedce2d..d346da6 100644 --- a/app/src/auth/auth-schema.ts +++ b/app/src/auth/auth-schema.ts @@ -1,6 +1,7 @@ import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; import { objectTransform, s } from "bknd/utils"; +import { $object } from "modules/mcp"; export const Strategies = { password: { @@ -36,7 +37,7 @@ export type AppAuthStrategies = s.Static; export type AppAuthOAuthStrategy = s.Static; export type AppAuthCustomOAuthStrategy = s.Static; -const guardConfigSchema = s.object({ +const guardConfigSchema = $object("config_auth_guard", { enabled: s.boolean({ default: false }).optional(), }); export const guardRoleSchema = s.strictObject({ @@ -45,7 +46,8 @@ export const guardRoleSchema = s.strictObject({ implicit_allow: s.boolean().optional(), }); -export const authConfigSchema = s.strictObject( +export const authConfigSchema = $object( + "config_auth", { enabled: s.boolean({ default: false }), basepath: s.string({ default: "/api/auth" }), diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 46dfc04..e775493 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -9,6 +9,7 @@ import type { ServerEnv } from "modules/Controller"; import { pick } from "lodash-es"; import { InvalidConditionsException } from "auth/errors"; import { s, parse, secret, runtimeSupports, truncate, $console } from "bknd/utils"; +import { $object } from "modules/mcp"; import type { AuthStrategy } from "./strategies/Strategy"; type Input = any; // workaround @@ -41,39 +42,38 @@ export interface UserPool { } const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds -export const cookieConfig = s - .object({ - path: s.string({ default: "/" }), - sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }), - secure: s.boolean({ default: true }), - httpOnly: s.boolean({ default: true }), - expires: s.number({ default: defaultCookieExpires }), // seconds - partitioned: s.boolean({ default: false }), - renew: s.boolean({ default: true }), - pathSuccess: s.string({ default: "/" }), - pathLoggedOut: s.string({ default: "/" }), - }) +export const cookieConfig = $object("config_auth_cookie", { + path: s.string({ default: "/" }), + sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }), + secure: s.boolean({ default: true }), + httpOnly: s.boolean({ default: true }), + expires: s.number({ default: defaultCookieExpires }), // seconds + partitioned: s.boolean({ default: false }), + renew: s.boolean({ default: true }), + pathSuccess: s.string({ default: "/" }), + pathLoggedOut: s.string({ default: "/" }), +}) .partial() .strict(); // @todo: maybe add a config to not allow cookie/api tokens to be used interchangably? // see auth.integration test for further details -export const jwtConfig = s - .object( - { - // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth - secret: secret({ default: "" }), - alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(), - expires: s.number().optional(), // seconds - issuer: s.string().optional(), - fields: s.array(s.string(), { default: ["id", "email", "role"] }), - }, - { - default: {}, - }, - ) - .strict(); +export const jwtConfig = $object( + "config_auth_jwt", + { + // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth + secret: secret({ default: "" }), + alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(), + expires: s.number().optional(), // seconds + issuer: s.string().optional(), + fields: s.array(s.string(), { default: ["id", "email", "role"] }), + }, + { + default: {}, + }, +).strict(); + export const authenticatorConfig = s.object({ jwt: jwtConfig, cookie: cookieConfig, diff --git a/app/src/cli/commands/index.ts b/app/src/cli/commands/index.ts index 9f63382..8e5b8b9 100644 --- a/app/src/cli/commands/index.ts +++ b/app/src/cli/commands/index.ts @@ -6,3 +6,4 @@ export { user } from "./user"; export { create } from "./create"; export { copyAssets } from "./copy-assets"; export { types } from "./types"; +export { mcp } from "./mcp/mcp"; diff --git a/app/src/cli/commands/mcp/mcp.ts b/app/src/cli/commands/mcp/mcp.ts new file mode 100644 index 0000000..f1df60f --- /dev/null +++ b/app/src/cli/commands/mcp/mcp.ts @@ -0,0 +1,97 @@ +import type { CliCommand } from "cli/types"; +import { makeAppFromEnv } from "../run"; +import { s } from "bknd/utils"; +import { ObjectToolSchema } from "modules/mcp"; +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { mcp as mcpMiddleware, McpServer, Resource } from "jsonv-ts/mcp"; +import type { Module } from "modules/Module"; + +export const mcp: CliCommand = (program) => + program + .command("mcp") + .description("mcp server") + .option("--port ", "port to listen on", "3000") + .option("--path ", "path to listen on", "/mcp") + .action(action); + +async function action(options: { port: string; path: string }) { + const app = await makeAppFromEnv({ + server: "node", + }); + + const appConfig = app.modules.configs(); + const { version, ...appSchema } = app.getSchema(); + + const schema = s.strictObject(appSchema); + + const nodes = [...schema.walk({ data: appConfig })].filter( + (n) => n.schema instanceof ObjectToolSchema, + ) as s.Node[]; + const tools = [...nodes.flatMap((n) => n.schema.getTools(n)), ...app.modules.ctx().mcp.tools]; + const resources = [...app.modules.ctx().mcp.resources]; + + const server = new McpServer( + { + name: "bknd", + version: "0.0.1", + }, + { app, ctx: () => app.modules.ctx() }, + tools, + resources, + ); + server + .resource("system_config", "bknd://system/config", (c) => + c.json(c.context.app.toJSON(), { + title: "System Config", + }), + ) + .resource( + "system_config_module", + "bknd://system/config/{module}", + (c, { module }) => { + const m = c.context.app.modules.get(module as any) as Module; + return c.json(m.toJSON(), { + title: `Config for ${module}`, + }); + }, + { + list: Object.keys(appConfig), + }, + ) + .resource("system_schema", "bknd://system/schema", (c) => + c.json(c.context.app.getSchema(), { + title: "System Schema", + }), + ) + .resource( + "system_schema_module", + "bknd://system/schema/{module}", + (c, { module }) => { + const m = c.context.app.modules.get(module as any); + return c.json(m.getSchema().toJSON(), { + title: `Schema for ${module}`, + }); + }, + { + list: Object.keys(appSchema), + }, + ); + + const hono = new Hono().use( + mcpMiddleware({ + server, + endpoint: { + path: String(options.path) as any, + }, + }), + ); + + serve({ + fetch: hono.fetch, + port: Number(options.port) || 3000, + }); + console.info(`Server is running on http://localhost:${options.port}${options.path}`); + console.info(`⚙️ Tools:\n${server.tools.map((t) => `- ${t.name}`).join("\n")}\n`); + console.info(`📚 Resources:\n${server.resources.map((r) => `- ${r.name}`).join("\n")}`); +} diff --git a/app/src/core/utils/index.ts b/app/src/core/utils/index.ts index 36928c5..30321ed 100644 --- a/app/src/core/utils/index.ts +++ b/app/src/core/utils/index.ts @@ -13,18 +13,4 @@ export * from "./uuid"; export * from "./test"; export * from "./runtime"; export * from "./numbers"; -export { - s, - stripMark, - mark, - stringIdentifier, - SecretSchema, - secret, - parse, - jsc, - describeRoute, - schemaToSpec, - openAPISpecs, - type ParseOptions, - InvalidSchemaError, -} from "./schema"; +export * from "./schema"; diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 0b4e464..fbe7514 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -1,5 +1,4 @@ -import { transformObject } from "core/utils"; - +import { transformObject } from "bknd/utils"; import { Module } from "modules/Module"; import { DataController } from "./api/DataController"; import { type AppDataConfig, dataConfigSchema } from "./data-schema"; @@ -49,10 +48,9 @@ export class AppData extends Module { this.ctx.em.addIndex(index); } - this.ctx.server.route( - this.basepath, - new DataController(this.ctx, this.config).getController(), - ); + const dataController = new DataController(this.ctx, this.config); + dataController.registerMcp(); + this.ctx.server.route(this.basepath, dataController.getController()); this.ctx.guard.registerPermissions(Object.values(DataPermissions)); this.setBuilt(); diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index b468a08..952ddb4 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -516,4 +516,35 @@ export class DataController extends Controller { return hono; } + + override registerMcp() { + this.ctx.mcp + .resource( + "data_entities", + "bknd://data/entities", + (c) => c.json(c.context.ctx().em.toJSON().entities), + { + title: "Entities", + description: "Retrieve all entities", + }, + ) + .resource( + "data_relations", + "bknd://data/relations", + (c) => c.json(c.context.ctx().em.toJSON().relations), + { + title: "Relations", + description: "Retrieve all relations", + }, + ) + .resource( + "data_indices", + "bknd://data/indices", + (c) => c.json(c.context.ctx().em.toJSON().indices), + { + title: "Indices", + description: "Retrieve all indices", + }, + ); + } } diff --git a/app/src/media/media-schema.ts b/app/src/media/media-schema.ts index a287d0a..af18e49 100644 --- a/app/src/media/media-schema.ts +++ b/app/src/media/media-schema.ts @@ -1,6 +1,7 @@ import { MediaAdapters } from "media/media-registry"; import { registries } from "modules/registries"; import { s, objectTransform } from "bknd/utils"; +import { $object } from "modules/mcp"; export const ADAPTERS = { ...MediaAdapters, @@ -22,7 +23,8 @@ export function buildMediaSchema() { ); }); - return s.strictObject( + return $object( + "config_media", { enabled: s.boolean({ default: false }), basepath: s.string({ default: "/api/media" }), diff --git a/app/src/modules/Controller.ts b/app/src/modules/Controller.ts index 51ae026..ac6808c 100644 --- a/app/src/modules/Controller.ts +++ b/app/src/modules/Controller.ts @@ -68,4 +68,6 @@ export class Controller { // @todo: current workaround to allow strings (sometimes building is not fast enough to get the entities) return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string(); } + + registerMcp(): void {} } diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 126a15e..60d6aab 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -7,6 +7,7 @@ import type { ModuleHelper } from "./ModuleHelper"; import { SchemaObject } from "core/object/SchemaObject"; import type { DebugLogger } from "core/utils/DebugLogger"; import type { Guard } from "auth/authorize/Guard"; +import type { McpServer } from "jsonv-ts/mcp"; type PartialRec = { [P in keyof T]?: PartialRec }; @@ -19,6 +20,7 @@ export type ModuleBuildContext = { logger: DebugLogger; flags: (typeof Module)["ctx_flags"]; helper: ModuleHelper; + mcp: McpServer<{ ctx: () => ModuleBuildContext }>; }; export abstract class Module { diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 42d9a94..9d90578 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -21,6 +21,7 @@ import { AppMedia } from "../media/AppMedia"; import type { ServerEnv } from "./Controller"; import { Module, type ModuleBuildContext } from "./Module"; import { ModuleHelper } from "./ModuleHelper"; +import { McpServer, type Resource, type Tool } from "jsonv-ts/mcp"; export type { ModuleBuildContext }; @@ -144,6 +145,7 @@ export class ModuleManager { server!: Hono; emgr!: EventManager; guard!: Guard; + mcp!: ModuleBuildContext["mcp"]; private _version: number = 0; private _built = false; @@ -271,6 +273,9 @@ export class ModuleManager { ? this.em.clear() : new EntityManager([], this.connection, [], [], this.emgr); this.guard = new Guard(); + this.mcp = new McpServer(undefined as any, { + ctx: () => this.ctx(), + }); } const ctx = { @@ -281,6 +286,7 @@ export class ModuleManager { guard: this.guard, flags: Module.ctx_flags, logger: this.logger, + mcp: this.mcp, }; return { @@ -702,7 +708,7 @@ export class ModuleManager { return { version: this.version(), ...schemas, - }; + } as { version: number } & ModuleSchemas; } toJSON(secrets?: boolean): { version: number } & ModuleConfigs { diff --git a/app/src/modules/mcp/$object.ts b/app/src/modules/mcp/$object.ts new file mode 100644 index 0000000..511c39a --- /dev/null +++ b/app/src/modules/mcp/$object.ts @@ -0,0 +1,136 @@ +import * as s from "jsonv-ts"; +import { type Resource, Tool, type ToolAnnotation, type ToolHandlerCtx } from "jsonv-ts/mcp"; +import { excludePropertyTypes, rescursiveClean } from "./utils"; +import { autoFormatString, getPath } from "bknd/utils"; +import type { App } from "App"; +import type { ModuleBuildContext } from "modules"; + +export interface McpToolOptions { + title?: string; + description?: string; + annotations?: ToolAnnotation; + tools?: Tool[]; + resources?: Resource[]; +} + +export interface ObjectToolSchemaOptions extends s.IObjectOptions { + mcp?: McpToolOptions; +} + +type AppToolContext = { + app: App; + ctx: () => ModuleBuildContext; +}; +type AppToolHandlerCtx = ToolHandlerCtx; + +export class ObjectToolSchema< + const P extends s.TProperties = s.TProperties, + const O extends s.IObjectOptions = s.IObjectOptions, +> extends s.ObjectSchema { + public readonly mcp: McpToolOptions; + private cleanSchema: s.ObjectSchema; + + constructor( + public name: string, + properties: P, + options?: ObjectToolSchemaOptions, + ) { + const { mcp, ...rest } = options || {}; + + super(properties, rest as any); + this.name = name; + this.mcp = mcp || {}; + this.cleanSchema = this.getCleanSchema(); + } + + private getMcpOptions(action: "get" | "update") { + const { tools, resources, ...rest } = this.mcp; + const label = (text?: string) => + text && [autoFormatString(action), text].filter(Boolean).join(" "); + return { + title: label(this.title ?? this.mcp.title), + description: label(this.description ?? this.mcp.description), + annotations: { + destructiveHint: true, + idempotentHint: true, + ...rest.annotations, + }, + }; + } + + private getCleanSchema() { + const props = excludePropertyTypes(this as any, [ObjectToolSchema]); + const schema = s.strictObject(props); + return rescursiveClean(schema, { + removeRequired: true, + removeDefault: false, + }) as s.ObjectSchema; + } + + private toolGet(node: s.Node) { + return new Tool( + [this.name, "get"].join("_"), + { + ...this.getMcpOptions("get"), + inputSchema: s.strictObject({ + path: s + .string({ + pattern: /^[a-zA-Z0-9_.]{0,}$/, + description: "(optional) path to the property to get, e.g. `key.subkey`", + }) + .optional(), + include_secrets: s.boolean({ default: false }).optional(), + }), + }, + async (params, ctx: AppToolHandlerCtx) => { + const configs = ctx.context.app.toJSON(params.include_secrets); + const config = getPath(configs, node.instancePath); + const value = getPath(config, params.path ?? []); + return ctx.json({ + path: params.path ?? "", + value: value ?? null, + }); + }, + ); + } + + private toolUpdate(node: s.Node) { + const schema = this.cleanSchema; + return new Tool( + [this.name, "update"].join("_"), + { + ...this.getMcpOptions("update"), + inputSchema: s.strictObject({ + full: s.boolean({ default: false }).optional(), + value: s + .strictObject(schema.properties as any) + .partial() as unknown as s.ObjectSchema, + }), + }, + async (params, ctx: AppToolHandlerCtx) => { + return ctx.json(params); + }, + ); + } + + getTools(node: s.Node): Tool[] { + const { tools = [] } = this.mcp; + return [this.toolGet(node), this.toolUpdate(node), ...tools]; + } + + override toJSON(): s.JSONSchemaDefinition { + const { toJSON, "~standard": _, mcp, cleanSchema, name, ...rest } = this; + return JSON.parse(JSON.stringify(rest)); + } +} + +export const $object = < + const P extends s.TProperties = s.TProperties, + const O extends ObjectToolSchemaOptions = ObjectToolSchemaOptions, +>( + name: string, + properties: P, + options?: s.StrictOptions, +): ObjectToolSchema & O => { + return new ObjectToolSchema(name, properties, options) as any; +}; diff --git a/app/src/modules/mcp/index.ts b/app/src/modules/mcp/index.ts new file mode 100644 index 0000000..95aed8f --- /dev/null +++ b/app/src/modules/mcp/index.ts @@ -0,0 +1 @@ +export * from "./$object"; diff --git a/app/src/modules/mcp/utils.spec.ts b/app/src/modules/mcp/utils.spec.ts new file mode 100644 index 0000000..8ac11f1 --- /dev/null +++ b/app/src/modules/mcp/utils.spec.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "bun:test"; +import { excludePropertyTypes, rescursiveClean } from "./utils"; +import { s } from "../../core/utils/schema"; + +describe("rescursiveOptional", () => { + it("should make all properties optional", () => { + const schema = s.strictObject({ + a: s.string(), + b: s.number(), + nested: s.strictObject({ + c: s.string().optional(), + d: s.number(), + nested2: s.record(s.string()), + }), + }); + + //console.log(schema.toJSON()); + console.log( + rescursiveClean(schema, { + removeRequired: true, + removeDefault: true, + }).toJSON(), + ); + /* const result = rescursiveOptional(schema); + expect(result.properties.a.optional).toBe(true); */ + }); + + it("should exclude properties", () => { + const schema = s.strictObject({ + a: s.string(), + b: s.number(), + }); + + console.log(excludePropertyTypes(schema, [s.StringSchema])); + }); +}); diff --git a/app/src/modules/mcp/utils.ts b/app/src/modules/mcp/utils.ts new file mode 100644 index 0000000..5c7ff5a --- /dev/null +++ b/app/src/modules/mcp/utils.ts @@ -0,0 +1,51 @@ +import * as s from "jsonv-ts"; +import { isPlainObject, transformObject } from "bknd/utils"; + +export function rescursiveClean( + input: s.Schema, + opts?: { + removeRequired?: boolean; + removeDefault?: boolean; + }, +): s.Schema { + const json = input.toJSON(); + + const removeRequired = (obj: any) => { + if (isPlainObject(obj)) { + if ("required" in obj && opts?.removeRequired) { + obj.required = undefined; + } + + if ("default" in obj && opts?.removeDefault) { + obj.default = undefined; + } + + if ("properties" in obj && isPlainObject(obj.properties)) { + for (const key in obj.properties) { + obj.properties[key] = removeRequired(obj.properties[key]); + } + } + } + + return obj; + }; + + removeRequired(json); + return s.fromSchema(json); +} + +export function excludePropertyTypes( + input: s.ObjectSchema, + props: (new (...args: any[]) => s.Schema)[], +): s.TProperties { + const properties = { ...input.properties }; + + return transformObject(properties, (value, key) => { + for (const prop of props) { + if (value instanceof prop) { + return undefined; + } + } + return value; + }); +} diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index 6a2f851..2927c42 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -1,25 +1,32 @@ import { Exception } from "core/errors"; import { isDebug } from "core/env"; import { $console, s } from "bknd/utils"; +import { $object } from "modules/mcp"; import { cors } from "hono/cors"; import { Module } from "modules/Module"; import { AuthException } from "auth/errors"; const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"] as const; -export const serverConfigSchema = s.strictObject({ - cors: s.strictObject({ - origin: s.string({ default: "*" }), - allow_methods: s.array(s.string({ enum: serverMethods }), { - default: serverMethods, - uniqueItems: true, +export const serverConfigSchema = $object( + "config_server", + { + cors: s.strictObject({ + origin: s.string({ default: "*" }), + allow_methods: s.array(s.string({ enum: serverMethods }), { + default: serverMethods, + uniqueItems: true, + }), + allow_headers: s.array(s.string(), { + default: ["Content-Type", "Content-Length", "Authorization", "Accept"], + }), + allow_credentials: s.boolean({ default: true }), }), - allow_headers: s.array(s.string(), { - default: ["Content-Type", "Content-Length", "Authorization", "Accept"], - }), - allow_credentials: s.boolean({ default: true }), - }), -}); + }, + { + description: "Server configuration", + }, +); export type AppServerConfig = s.Static; diff --git a/bun.lock b/bun.lock index 1bf0fac..8fb8b89 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.16.0-rc.0", + "version": "0.16.0", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -71,7 +71,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.3.2", + "jsonv-ts": "^0.5.1", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", @@ -2511,7 +2511,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.3.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-wGKLo0naUzgOCa2BgtlKZlF47po7hPjGXqDZK2lOoJ/4sE1lb4fMvf0YJrRghqfwg9QNtWz01xALr+F0QECYag=="], + "jsonv-ts": ["jsonv-ts@0.5.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-9PoYn7rfk67q5aeDQLPcGUHJ1YWS/f4BPPsWY0HOna6hVKDbgRP+kem9lSvmf8IfQCpq363xVmR/g49MEMGOdQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],