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.
| Item | Value |
|---|---|
| Previous release | v0.1.3, commit 03eeeee |
| Current release | v0.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 operation | HTTP 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
fetchimplementation, 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
| Failure | Client behavior |
|---|---|
| Declared domain error with a code | Throws ActionExecutionError with the server's code and status |
| Validation error | Throws ValidationError, including field-level errors |
| Unexpected server failure | Throws BrickServerError |
| Network failure, malformed response, or other transport failure | Throws BrickTransportError |
| Request cancellation | Preserves 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
$clientproperty. - 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 } });| Request | Behavior |
|---|---|
initialize | Returns server information, tools capability, and negotiated protocol version |
ping | Returns an empty result |
tools/list | Returns registered tool definitions |
tools/call | Dispatches a tool and returns text content |
| Notification without an ID | Returns HTTP 202 with no response body |
| Non-POST request | Returns 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
- Upgrade
@brickkit/coreand@brickkit/clitogether to0.2.0. - Replace
createBrickServerimports and calls withbrick. - Define a contract and use
brickFromContractif you want a shared server/client contract; explicitbrick({ services })remains supported. - Use
@brickkit/core/clientin browser code and import the backend contract only as a type. - Remove CRUD wrappers that only supplied missing input/output types. Let the resource and SDK infer them.
- Handle projected list rows as partial values instead of assuming a complete record.
- Supply
clientto a Postgres definition when the application already owns a postgres-js client. Keep client configuration and shutdown with its owner. - Convert intended MCP definitions to
.tool()and place annotations with them. - Explicitly opt tools into HTTP when needed; include only HTTP-enabled tools in browser contracts.
- 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.
| Commit | Framework change |
|---|---|
cd1cad4 | Introduced brick() as the preferred server factory |
af38cc3 | Added the contract-inferred typed client and contract-based server setup |
c1ac21d | Added injected Postgres client support |
d6ec499 | Fixed client response parsing and absent object input in HTTP handlers |
a3ebb37 | Fixed client declaration chunk placement and Web-compatible fetch typing while integrating the local application |
5d76587 | Preserved resource CRUD schema types through the SDK |
6798377 | Added tools, annotations, shared MCP helpers, and the tools documentation |
7b13ec8 | Removed the deprecated createBrickServer alias |
b4aebe7 | Set 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.