brick.ts
All posts
Framework updatev0.2.0

BrickKit v0.2.0: typed clients, MCP tools, and shared database connections

What changed since v0.1.3, how the new APIs work, and how to upgrade your BrickKit app.

By BrickKit team

This document describes changes to @brickkit/core and @brickkit/cli between the previous release and the current release. It focuses on public APIs, runtime behavior, typing, and migration steps.

ItemValue
Previous releasev0.1.3, commit 03eeeee
Current releasev0.2.0, commit b4aebe7
Core package@brickkit/core@0.2.0
CLI package@brickkit/cli@0.2.0
CLI dependency on core^0.2.0

The full repository comparison contains 14 commits. Website redesigns, example-app restructuring, benchmark changes, formatting-only edits, and release automation are outside this framework report.

1. The server entry point is now brick()

The core package's server factory is now named brick. The old createBrickServer export has been removed.

Before:

import { createBrickServer } from "@brickkit/core";

const app = createBrickServer({ services: [pagesService] });

After:

import { brick } from "@brickkit/core";

const app = brick({ services: [pagesService] });
app.listen(4000);

The rename does not introduce a different server implementation. The existing options, Elysia integration, resource routes, compiled handlers, raw endpoints, and documentation endpoints continue through brick().

CreateServerOptions is still the exported options type. This release removes the old function name, not that type name.

Migration: replace imports and calls to createBrickServer with brick. Code still importing the old name will need updating before it can use 0.2.0.

Implementation: server.ts, CLI exports.

API update: the contract-based server helpers described below have been removed. Use brick({ services }) for server construction; see the current Typed Client guide for client setup.

2. A typed HTTP client derived from the server contract

The framework now includes createBrickClient, an HTTP client whose input and output types derive from actual action and resource definitions.

You can use it without generating a client from OpenAPI. OpenAPI remains available for API documentation and other consumers.

Define the contract on the backend

// contract.ts
import { defineAppContract } from "@brickkit/core/client";
import { pagesResource, billingService } from "./services";
import {
  ChargeInputSchema,
  ChargeResultSchema,
  chargeCustomer,
} from "./billing";

export const contract = defineAppContract({
  page: pagesResource,
  billing: {
    charge: billingService.action({
      name: "charge",
      input: ChargeInputSchema,
      output: ChargeResultSchema,
      execute: chargeCustomer,
    }),
  },
});

export type AppApi = typeof contract;

defineAppContract preserves the types of the definitions placed in the object. It does not generate files or create a second schema system.

Use the contract type in the browser

import { createBrickClient } from "@brickkit/core/client";
import type { AppApi } from "./contract";

const api = createBrickClient<AppApi>({
  baseUrl: "http://localhost:4000",
});

const page = await api.page.create({ title: "Hello", content: "Body" });
const result = await api.page.list({ limit: 20 });

The contract import is type-only and disappears from the browser bundle. Use the @brickkit/core/client entry point for browser code; the helpers are also re-exported from @brickkit/core.

Routing

Client operationHTTP request
api.page.list(input)GET /api/page with query parameters
api.page.create(input)POST /api/page with a JSON body
api.page.get({ id })GET /api/page/:id
api.page.update({ id, ...changes })PATCH /api/page/:id with the changes in the body
api.page.delete({ id })DELETE /api/page/:id
api.billing.charge(input)POST /api/billing/charge with a JSON body

By default, contract keys must match backend service and resource names. This mode works with a type-only contract import.

When the runtime contract is available, pass contract to the client. It resolves service, action, and resource names from their bound metadata, allowing different organizational keys:

const api = createBrickClient<AppApi>({
  baseUrl: "http://localhost:4000",
  contract,
});

Custom action namespaces can represent dotted action names. Custom action calls use the service/action HTTP endpoint; the client is not an arbitrary route-template generator.

Request configuration

The client supports:

  • A custom API prefix, defaulting to /api.
  • Default headers, including an asynchronous header provider for refreshed credentials.
  • An injected fetch implementation, useful for cookies and in-process tests.
  • Default and per-call abort signals and timeouts.
  • Per-call headers merged over the defaults.

The custom-fetch type accepts a normal Web-compatible function. It does not require Bun-specific properties such as fetch.preconnect.

Errors and response parsing

FailureClient behavior
Declared domain error with a codeThrows ActionExecutionError with the server's code and status
Validation errorThrows ValidationError, including field-level errors
Unexpected server failureThrows BrickServerError
Network failure, malformed response, or other transport failureThrows BrickTransportError
Request cancellationPreserves the abort error

The client handles JSON responses and plain-text action results. A text/plain response remains text. If no content type is supplied, it attempts JSON parsing and can fall back to text. A malformed response explicitly labeled as JSON still fails.

New typing helpers include BrickClient, BrickActionCaller, BrickResourceClient, InferActionInput, InferActionOutput, InferActionErrorCodes, and InferActionFailure. Failure helpers describe the possible error shape; TypeScript promises do not encode thrown exceptions in their return type.

Implementation: client.ts. Guide: Typed Client.

3. Build the HTTP server from the same contract

brickFromContract is a new core helper that derives the participating services from the contract:

import { brickFromContract } from "@brickkit/core";
import { contract } from "./contract";

const app = brickFromContract(contract, { port: 4000 });
app.listen(4000);

It accepts the normal server options except services, which it resolves using the definitions' bound service names and the global registry.

Contracts can contain actions, resources, services, and nested namespaces. Definitions must be bound to registered services. Invalid or empty contracts, unbound actions, and unknown services produce explanatory errors.

Important behavior: the contract identifies participating services. The resulting server mounts the HTTP actions registered on those services. Selecting one action in a contract is not an allowlist that hides other HTTP actions on the same service.

MCP-only tools can identify a participating service without becoming HTTP routes. Tool-only services are supported; their tools remain absent from routes and OpenAPI unless explicitly enabled for HTTP.

Implementation: contract.ts.

4. Resource CRUD types no longer erase the table schema

Previously, the resource's public CRUD members were declared with Action<any, any, any, TCtx>. Even if an application wrapped them with a typed function, the underlying resource and SDK caller still accepted and returned any.

Resources now retain the table type and configuration literals when generating the public action types. This works through both service.resource(...) and defineResource(...).

Input types

  • Create inputs retain required columns and their value types.
  • Columns with defaults and nullable columns retain their optional create-input behavior.
  • Generated primary keys, configured ID fields, owner fields, and recognized creation/update timestamps are excluded from editable inputs.
  • Update inputs require the configured ID and accept partial editable fields.
  • Get and delete inputs use the configured ID field.
  • List filters retain their column types, alongside pagination, sorting, search, projection, and cursor options.
  • Enum values and TypeBox field overrides are reflected in the inferred types.
  • Owner and timestamp detection accounts for relevant JavaScript keys and database column names.

ID request values follow the runtime resource schema, which uses strings. A custom idField changes the resource action's input key. The HTTP client's resource item routing still extracts id or ID, so a differently named action ID key needs corresponding transport integration.

Nullable database columns do not automatically make explicit null valid in create input. The default runtime create schema makes those fields optional and expects their non-null value type. An explicit field-schema override can change the allowed values.

Output types and list projections

Create, get, and update results are typed from the resource row. List results contain partial rows because select and excludeFromList can omit columns. For example, a list may omit content even though an individual get returns it.

The standard list result includes items, total, limit, offset, optional page, pageCount, hasMore, and nextCursor.

Applications must account for projected fields instead of assuming that every list item is a complete row. If a collection requires an ID, check that the returned projection includes it before using it as a collection key.

Application wrappers can disappear

Before:

const pageApi = {
  create: (input: PageCreateInput): Promise<Page> => api.page.create(input),
};

After:

const pageApi = api.page;
type Page = Awaited<ReturnType<typeof pageApi.get>>;
type PageCreateInput = Parameters<typeof pageApi.create>[0];

Missing required fields, incorrect column values, invalid enums, and attempts to supply generated fields can now fail at compile time without these wrappers.

Input inference reads the action's schema first, preserving explicit undefined input support where applicable, including calling a resource list without arguments. A table deliberately typed as any cannot provide precise column types and retains the dynamic fallback.

Implementation: resource types, resource creation, type regression checks.

5. Postgres accepts an existing postgres-js client

Postgres now supports the injected-client pattern already available for SQLite:

import postgres from "postgres";
import { defineDatabase } from "@brickkit/core";
import { pagesTable } from "./schema";

const sql = postgres(process.env.DATABASE_URL!, { prepare: false });

const handle = defineDatabase({
  engine: "postgres",
  client: sql,
  tables: { pagesTable },
});

const db = handle.getDb();
// db.$client is the same postgres-js client as sql.

client?: Sql is accepted by the Postgres configuration and database definition. It is preferred over opening a new connection from a URL or host configuration.

When a client is provided:

  • Connection settings belong to that client; Brick does not open a second pool.
  • URL, host, port, credentials, pool size, SSL, and prepared-statement settings do not reconfigure the supplied client.
  • Unused connection secrets are not resolved by getDb() solely to build a connection that will not be used.
  • Drizzle schema configuration still applies.
  • The Postgres handle's result exposes the typed $client property.
  • The caller owns the client's lifecycle.

Existing URL/host-based definitions continue to work when no client is supplied. This is an optional addition, not a requirement to change every database definition.

This allows an application to share one connection pool between Brick services, relational Drizzle queries, authentication adapters, and other consumers. It introduces no Frappe-specific adapter or changes to @pw/db.

Implementation: Postgres adapter, database definitions, legacy database entry point.

6. MCP tools are first-class service definitions

The framework now provides service.tool(...) and defineTool(...). Tools reuse action execution, validation, authorization, declared errors, and service context, while registering separately from HTTP actions.

import { defineService, t } from "@brickkit/core";

const crm = defineService("crm");

const whoami = crm.tool({
  name: "crm_whoami",
  title: "View Connected CRM Account",
  description: "Return the authenticated account ID.",
  annotations: { readOnlyHint: true },
  input: t.Object({}, { additionalProperties: false }),
  output: t.String(),
  authorize: ({ user }) => !!user?.id,
  execute: ({ ctx }) => ctx.user.id,
});

Tool input schemas must be object schemas. Input/output types and context inference follow the action API, including narrowing a nullable user when an authorization function is declared.

Tools are directly callable and support the action-style execution methods. A standalone defineTool(...) result can be attached using service.tool(tool).

Annotations live on the definition

The annotation type supports:

  • title.
  • readOnlyHint.
  • destructiveHint.
  • idempotentHint.
  • openWorldHint.

The top-level tool title is used when present; annotations.title provides a fallback. These are metadata hints for clients, not authorization enforcement. Applications still declare authorize for permission checks.

There is no requirement for a parallel TOOL_ANNOTATIONS map.

HTTP exposure is explicit

By default, a tool does not create an HTTP action route and does not appear in OpenAPI.

Either opt in at definition time:

const echo = crm.tool({
  name: "echo",
  http: true,
  input: t.Object({ message: t.String() }),
  output: t.String(),
  execute: ({ input }) => input.message,
});

Or attach an existing tool as an HTTP action:

crm.action(whoami);

Both preserve MCP registration while enabling normal HTTP routing and OpenAPI generation. Merely referencing a tool or service in brickFromContract does not opt that tool into HTTP.

Services expose separate tools, getTool(name), and listTools() APIs. Ordinary .action() definitions are not automatically exposed as MCP tools.

Implementation: tool.ts, service.ts, tool types.

7. Shared MCP registry and JSON-RPC handler

The framework replaces repeated application-level listing and dispatch code with reusable helpers.

Registry

import { createMcpRegistry } from "@brickkit/core";

const registry = createMcpRegistry({ services: [crm] });

registry.listTools();
await registry.callTool("crm_whoami", {}, { user: { id: "user_1" } });

The registry reads names, descriptions, object input schemas, titles, and annotations from registered tool definitions. Dispatch calls the actual tool, retaining the framework's validation and authorization behavior.

It accepts multiple services, an explicit tools array, or both. Different tools with duplicate names are rejected. It reads service registrations when listing or calling, so later registrations are included. Unknown tools throw the exported UnknownToolError.

Organize definitions under tools/ and import the modules to register them. A service-based registry removes the need for a separately maintained tool array or a custom name-search loop.

JSON-RPC handler

import { createMcpHandler } from "@brickkit/core";

const handleMcp = createMcpHandler({
  registry,
  serverInfo: { name: "crm-agent", version: "1.0.0" },
});

// In the app's authenticated HTTP handler:
// return handleMcp(request, { user: { id: verifiedSubject } });
RequestBehavior
initializeReturns server information, tools capability, and negotiated protocol version
pingReturns an empty result
tools/listReturns registered tool definitions
tools/callDispatches a tool and returns text content
Notification without an IDReturns HTTP 202 with no response body
Non-POST requestReturns HTTP 405

The default protocol version is 2025-06-18. An explicit protocolVersions list can be supplied, newest first. Initialization uses the requested version when supported, otherwise the first configured version.

Malformed JSON, invalid requests, invalid tool-call parameters, unknown tool names, and unknown methods produce JSON-RPC errors. Tool input-validation, authorization, and domain failures are returned as tool results with isError: true.

Strings are returned as text; other values are serialized as JSON text. The helper is a stateless POST JSON-RPC handler. It does not supply token verification, OAuth discovery, sessions, or a general streaming transport. Those remain in the application. The incoming Request is passed to service context resolution.

The exported types include McpToolDefinition, McpRegistry, McpRegistryOptions, and McpHandlerOptions.

Implementation: mcp.ts. Guide: MCP Tools.

8. Framework fixes supporting these APIs

Empty object input in HTTP handlers

When a route expects an object input schema and a request has no assembled input, the runtime handler now supplies {} instead of validating undefined.

The generated compiler handler follows the same rule. This fixes requests such as an unfiltered resource list in a built server. Required fields still undergo normal schema validation; supplying {} does not make a required payload valid.

Implementation: runtime action handler, emitted handlers.

Published client declarations

The package now builds and exports the @brickkit/core/client entry point. Shared build chunks live under chunks/, preventing shared declaration output from overwriting the public client declaration entry.

This makes the browser client available with its declarations through normal package resolution.

Implementation: core package exports, build configuration.

OpenAPI service typing

The OpenAPI generator's service option accepts services with concrete database and context types through Service<any, any>[]. This avoids rejecting valid typed services when generating a spec. It does not change the generated OpenAPI format.

Implementation: openapi.ts.

9. Upgrade checklist

  1. Upgrade @brickkit/core and @brickkit/cli together to 0.2.0.
  2. Replace createBrickServer imports and calls with brick.
  3. Define a contract and use brickFromContract if you want a shared server/client contract; explicit brick({ services }) remains supported.
  4. Use @brickkit/core/client in browser code and import the backend contract only as a type.
  5. Remove CRUD wrappers that only supplied missing input/output types. Let the resource and SDK infer them.
  6. Handle projected list rows as partial values instead of assuming a complete record.
  7. Supply client to a Postgres definition when the application already owns a postgres-js client. Keep client configuration and shutdown with its owner.
  8. Convert intended MCP definitions to .tool() and place annotations with them.
  9. Explicitly opt tools into HTTP when needed; include only HTTP-enabled tools in browser contracts.
  10. Replace repeated MCP list/dispatch/JSON-RPC switches with the registry and handler, keeping authentication in the app.

10. Validation and traceability

Validation performed during implementation included:

  • 121 passing framework and benchmark tests after the tools changes.
  • 71 passing Postgres-backed Pages integration tests using local framework packages.
  • Framework, CLI, and application type checks.
  • Compile-time regression checks for required CRUD inputs, column types, enums, generated fields, overrides, projected rows, and tool context/output inference.
  • Tests confirming MCP-only definitions do not create HTTP routes or OpenAPI entries, including tool-only contract services.
  • Tests for HTTP opt-in, duplicate tool names, tool dispatch, validation, authorization, notifications, and JSON-RPC error behavior.
  • Package builds and a production build of the combined Pages application.
  • Docs type checking and a website build that prerendered the new tools guide.

These counts describe the implementation checks. They are not performance claims. The v0.2.0 publication workflow also completed successfully.

CommitFramework change
cd1cad4Introduced brick() as the preferred server factory
af38cc3Added the contract-inferred typed client and contract-based server setup
c1ac21dAdded injected Postgres client support
d6ec499Fixed client response parsing and absent object input in HTTP handlers
a3ebb37Fixed client declaration chunk placement and Web-compatible fetch typing while integrating the local application
5d76587Preserved resource CRUD schema types through the SDK
6798377Added tools, annotations, shared MCP helpers, and the tools documentation
7b13ec8Removed the deprecated createBrickServer alias
b4aebe7Set both packages to 0.2.0 and updated the CLI's core dependency

Release: v0.2.0. Comparison: v0.1.3…v0.2.0. Successful publication run: GitHub Actions.