Silkweave
Write your logic once. Run it everywhere.
Silkweave is a TypeScript toolkit that lets you define application logic as portable Actions and instantly expose them through any combination of transports - MCP servers (stdio, HTTP, and serverless), end-to-end-typed tRPC, REST APIs with auto-generated OpenAPI docs, and fully-featured CLIs. It also meets your framework where it is: expose existing NestJS controllers as MCP tools, project one action set onto Next.js App Router route handlers, or stream to the Vercel AI SDK's useChat. No glue code required.
┌───────────────────────────────────┐
│ ACTIONS │
│ name + zod schema + async run() │
└─────────────────┬─────────────────┘
│
ADAPTER ┌────────────────┬────────┼────────┬────────────────┐
│ │ │ │ │
┌───────▼──────┐ ┌───────▼──────┐ │ ┌──────▼───────┐ ┌──────▼───────┐
│ MCP │ │ API │ │ │ CLI │ │ EDGE │
│ Http/Stdio │ │ tRPC/Fastify │ │ │ Commander │ │ Serverless │
└──────────────┘ └──────────────┘ │ └──────────────┘ └──────────────┘
│
FEATURE ┌───────────────────────┼───────────────────────┐
│ │ │
┌─────────▼─────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ AUTHENTICATION │ │ CONTEXT BAG │ │ TYPE GENERATION │
│ Oauth2/Bearer/JWT │ │ Typed/Extensible │ │ Input/Output DTS │
└───────────────────┘ └───────────────────┘ └───────────────────┘
Table of Contents
- Why Silkweave
- Packages
- Quick Start
- Core Concepts
- Adapters in Depth
- Framework Integrations
- Agent Skills over MCP
- Authentication
- Logging and Progress
- Advanced Patterns
- MCP Client Configuration
- API Reference
- Development
Why Silkweave
Building an MCP server usually means wiring up transports, registering tools, serializing responses, and handling errors - for every single tool. If you also want a CLI or REST API for the same logic, you're writing it all again.
Silkweave eliminates this duplication. You define an Action - a name, a Zod schema, and an async function - and Silkweave handles the rest:
- MCP adapters register your actions as MCP tools with proper notifications, progress reporting, and error handling - over stdio, streamable HTTP, or stateless serverless (Edge / Web Standard) - plus agent-quality signals: tool annotations, typed output contracts, per-request tool filtering, and call telemetry
- tRPC adapter exposes your actions as end-to-end type-safe procedures (
InferTrpcRouter<typeof server>), as a standalone server or a fetch handler - Fastify adapter generates a REST API with full OpenAPI/Swagger documentation derived from your Zod schemas
- CLI adapter builds a complete command-line interface with argument parsing, option flags, and plain
consoleoutput - Framework integrations meet you where you are - expose existing NestJS controllers as MCP tools via
@Mcp(), project an action set onto Next.js App Router handlers (MCP + tRPC), or bridge a streaming action to the Vercel AI SDK'suseChat
Your action doesn't know or care which transport is running it.
Packages
Silkweave is organized as a monorepo with modular packages. Install only what you need:
| Package | npm | Description |
|---|---|---|
@silkweave/core |
Core library - actions, adapters, builder, context, logger, utilities | |
@silkweave/auth |
Auth - resource-server core (bearer-token validation + protected-resource metadata, RFC 9728); opt-in OAuth 2.1 authorization-server proxy (PKCE, refresh, CIMD, dynamic client registration) via @silkweave/auth/oauth |
|
@silkweave/mcp |
MCP adapters - stdio, streamable HTTP, CLI proxy | |
@silkweave/cli |
CLI adapter - commander + plain console output |
|
@silkweave/fastify |
Fastify REST adapter - auto-generated OpenAPI/Swagger docs | |
@silkweave/trpc |
tRPC adapter - end-to-end type-safe procedures (standalone server + fetch handler) | |
@silkweave/edge |
Web-Standard edge/serverless adapter - stateless MCP over Streamable HTTP (Cloudflare Workers, Vercel, Bun, Deno) | |
@silkweave/nestjs |
NestJS adapter - expose existing controllers as MCP tools via @Mcp() (input reflected from route + param decorators + swagger/class-validator) |
|
@silkweave/nextjs |
Next.js App Router adapter - defineSilkweave({ actions }) projects one action set onto MCP + tRPC route handlers |
|
@silkweave/ai |
Vercel AI SDK bridge - wrap streamText as a streaming action and feed useChat over a tRPC subscription |
|
@silkweave/typegen |
Type generator - emit .d.ts interfaces from action Zod schemas |
|
@silkweave/skills |
Serve Agent Skills (SKILL.md) over MCP - versioned, digest-verified, SEP-2640-aligned |
|
silkweave |
The Silkweave CLI - npx silkweave skills sync (install/update skills from a server), skills pack (publish a skill as a Claude Code plugin), and npx silkweave proxy <url> (any MCP server as a CLI). 5.0 breaking: previously an umbrella of re-exports (silkweave/core, ...) - depend on the scoped @silkweave/* packages instead |
@silkweave/core is always required. Then add the adapter packages for the transports you need:
# MCP server (stdio or HTTP)
pnpm add @silkweave/core @silkweave/mcp
# tRPC (end-to-end typed)
pnpm add @silkweave/core @silkweave/trpc
# REST API with Swagger
pnpm add @silkweave/core @silkweave/fastify
# CLI tool
pnpm add @silkweave/core @silkweave/cli
# Edge / Web Standard serverless MCP (Cloudflare Workers, Vercel, Bun, Deno)
pnpm add @silkweave/core @silkweave/edge
# NestJS controllers as MCP tools
pnpm add @silkweave/core @silkweave/nestjs
# Next.js App Router (MCP + tRPC)
pnpm add @silkweave/core @silkweave/nextjs
# Serve Agent Skills from an MCP server (then: npx silkweave skills sync)
pnpm add @silkweave/core @silkweave/mcp @silkweave/skills
Express-Optional MCP
express and cors are optional peerDependencies of @silkweave/mcp - they are only needed for the Express-based http() server. The transport-agnostic tool-registration and result helpers are re-exported from the express-free @silkweave/mcp/tools subpath, which the Web-Standard adapters (@silkweave/edge, @silkweave/nextjs) import. This means a serverless bundle or install never pulls Express into its graph.
// Express-free: tool-registration + result helpers, no express/cors
import { registerTools, smartToolResult } from '@silkweave/mcp/tools'
If you use the Express http() server, install the peers:
pnpm add @silkweave/core @silkweave/mcp express cors
Quick Start
pnpm add @silkweave/core @silkweave/mcp
Create an action:
// actions/greet.ts
import z from 'zod/v4'
import { createAction } from '@silkweave/core'
export const GreetAction = createAction({
name: 'greet',
description: 'Greet someone by name',
input: z.object({
name: z.string().describe('The name to greet'),
enthusiastic: z.boolean().describe('Add excitement').default(false)
}),
run: async ({ name, enthusiastic }, { logger }) => {
const greeting = enthusiastic ? `HELLO, ${name.toUpperCase()}!!!` : `Hello, ${name}.`
logger.info(greeting)
return { greeting }
}
})
Serve it as an MCP server:
// server.ts
import { silkweave } from '@silkweave/core'
import { stdio } from '@silkweave/mcp'
import { GreetAction } from './actions/greet.js'
await silkweave({ name: 'my-server', description: 'My MCP Server', version: '1.0.0' })
.adapter(stdio())
.action(GreetAction)
.start()
That's it. Your action is now an MCP tool called Greet that any MCP client (Claude Desktop, Cursor, Claude Code, etc.) can discover and invoke.
Core Concepts
Actions
An Action is the fundamental unit of logic in Silkweave. It is completely transport-agnostic.
import z from 'zod/v4'
import { createAction } from '@silkweave/core'
export const SearchAction = createAction({
name: 'search',
description: 'Search documents by query',
input: z.object({
query: z.string().describe('Search query'),
limit: z.number().int().min(1).max(100).describe('Max results').default(10),
includeArchived: z.boolean().describe('Include archived documents').default(false)
}),
run: async ({ query, limit, includeArchived }, { logger }) => {
logger.info(`Searching for: ${query}`)
// ... your logic here
const results = await performSearch(query, { limit, includeArchived })
return { results, count: results.length }
}
})
createAction accepts an object with:
| Field | Type | Description |
|---|---|---|
name |
string |
Unique identifier. Adapters transform this automatically (PascalCase for MCP tools, kebab-case for CLI commands, as-is for REST routes). |
description |
string |
Human-readable description. Shown in MCP tool listings, CLI help, and Swagger docs. |
input |
z.ZodObject |
A Zod object schema defining the input. .describe() on each field provides per-field documentation across all adapters. |
args |
(keyof I)[] |
(Optional) Fields to expose as positional CLI arguments instead of --options. Only relevant for the CLI adapter. |
run |
(input, context) => Promise<O> |
The implementation. Receives validated input and an SilkweaveContext with a logger. Returns any object - adapters handle serialization. |
Adapters
Adapters are the bridge between your actions and the outside world. Each adapter is a factory function that takes configuration and returns a generator compatible with the Silkweave builder.
import { stdio, http } from '@silkweave/mcp'
import { fastify } from '@silkweave/fastify'
import { cli } from '@silkweave/cli'
// No config needed
stdio()
// Host and port required
http({ host: 'localhost', port: 8080 })
// Full Fastify options pass-through
fastify({ host: 'localhost', port: 8080, logger: true })
// No config needed
cli()
The adapter lifecycle:
- Factory -
stdio()/http({ ... })/ etc. captures configuration - Generator - Silkweave calls the factory result with
{ name, description, version }to produce anAdapter - Start -
adapter.start(actions)registers all actions and begins listening - Stop -
adapter.stop()tears down gracefully
The Silkweave Builder
The builder provides a fluent, chainable API:
import { silkweave } from '@silkweave/core'
import { stdio, http } from '@silkweave/mcp'
const app = silkweave({
name: 'my-toolkit',
description: 'A collection of useful tools',
version: '2.1.0'
})
app
.adapter(stdio()) // Add an adapter
.adapter(http({ ... })) // Add another - they run in parallel
.action(SearchAction) // Mount an action
.action(GreetAction) // Mount another
await app.start() // Start all adapters concurrently
.adapter() and .action() return the same instance, so you can chain freely. .start() launches all adapters in parallel via Promise.all.
Adapters in Depth
MCP Stdio
The standard MCP transport for local tool servers. Communicates over stdin/stdout using the MCP protocol.
import { silkweave } from '@silkweave/core'
import { stdio } from '@silkweave/mcp'
await silkweave({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
.adapter(stdio())
.action(MyAction)
.start()
How actions become MCP tools:
| Action property | MCP tool property |
|---|---|
name: 'searchDocs' |
Tool name: SearchDocs (PascalCase) |
description |
Tool description |
input (Zod schema) |
inputSchema (JSON Schema via Zod) |
| Return value | TextContent JSON response |
| Thrown errors | Structured error response with name, message, and stack |
MCP logging notifications are wired automatically - logger.info("message") in your action sends a notifications/message to the MCP client. Progress reporting works via logger.progress() when the client provides a progress token.
Claude Desktop / Claude Code configuration:
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["path/to/server.js"]
}
}
}
MCP Streamable HTTP
A stateless MCP transport over HTTP (per the 2026 spec direction): each POST /mcp mints a fresh transport and server, handles exactly that request - streaming progress over SSE when the call carries a progress token - and tears down when the response closes. No session IDs, no session map; any request can hit any instance, so the same adapter works behind load balancers and in multi-replica deployments.
import { silkweave } from '@silkweave/core'
import { http } from '@silkweave/mcp'
await silkweave({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
.adapter(http({
host: 'localhost',
port: 8080,
allowedHosts: ['localhost']
}))
.action(MyAction)
.start()
Endpoints exposed:
| Method | Path | Purpose |
|---|---|---|
POST |
/mcp |
Handle a single MCP request (per-request SSE response) |
GET |
/resource/:id |
Sideloaded embedded resources (disposition: 'smart') |
| various | OAuth + /.well-known/* |
When auth is configured |
GET |
/.claude-plugin/marketplace.json |
When skillsMarketplace is configured |
CORS is configured out of the box.
StartMcpHttpOptions (selected):
| Option | Type | Description |
|---|---|---|
host / port |
string / number |
Bind address and listen port |
allowedHosts |
string[] |
DNS-rebinding protection allow-list |
auth |
AuthConfig |
Bearer-token validation and/or OAuth 2.1 routes |
filterActions |
FilterActions |
Per-request tool filtering (see MCP Tool Quality) |
onToolCall |
OnToolCall |
Fire-and-forget per-call telemetry |
skills |
(Skill | SkillDefinition)[] |
Serve Agent Skills |
skillsMarketplace |
SkillsMarketplaceOptions |
Claude Code plugin marketplace for npm-published skills |
tRPC
Exposes your actions as end-to-end type-safe tRPC procedures. Each action becomes a query or mutation (per its kind) at camelCase(action.name), and the exported InferTrpcRouter<typeof server> gives your client a fully-typed AppRouter - no code generation.
import { silkweave } from '@silkweave/core'
import { type InferTrpcRouter, trpc } from '@silkweave/trpc'
const server = silkweave({ name: 'my-api', description: 'My API', version: '1.0.0' })
.adapter(trpc({ host: 'localhost', port: 8080 }))
.action(SearchAction)
export type AppRouter = InferTrpcRouter<typeof server>
await server.start() // tRPC server on http://localhost:8080/trpc/
// client.ts - fully typed against your actions
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from './server.js'
const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: 'http://localhost:8080/trpc' })] })
const { results } = await client.search.query({ query: 'hello', limit: 5 })
For serverless runtimes (Astro, Vercel, Cloudflare Workers), use trpcFetch() instead - it returns a Web Standard (Request) => Promise<Response> handler (plus GET/POST) rather than binding a port. Streaming actions are registered as tRPC subscriptions automatically.
Fastify REST API
Turns your actions into a REST API with auto-generated OpenAPI documentation and an interactive Swagger UI powered by Scalar.
import { silkweave } from '@silkweave/core'
import { fastify } from '@silkweave/fastify'
await silkweave({ name: 'my-api', description: 'My REST API', version: '1.0.0' })
.adapter(fastify({
host: 'localhost',
port: 8080,
logger: true
}))
.action(SearchAction)
.action(GreetAction)
.start()
Route mapping:
Each action becomes a POST /{action.name} route. The Zod schema is converted to JSON Schema for request body validation and OpenAPI documentation.
| Action | Route | Body |
|---|---|---|
name: 'search' |
POST /search |
{ "query": "...", "limit": 10 } |
name: 'greet' |
POST /greet |
{ "name": "World" } |
Visit http://localhost:8080/ for the interactive Scalar API reference with try-it-out functionality.
FastifyAdapterOptions:
Extends Fastify's native FastifyHttpOptions, so any Fastify config is supported:
fastify({
host: 'localhost',
port: 8080,
logger: {
level: 'debug',
transport: { target: 'pino-pretty' }
},
connectionTimeout: 30000
})
CLI
Transforms your actions into a complete command-line application with help text, option parsing, and plain console output.
import { silkweave } from '@silkweave/core'
import { cli } from '@silkweave/cli'
await silkweave({ name: 'mytool', description: 'My CLI Tool', version: '1.0.0' })
.adapter(cli())
.action(GreetAction)
.action(SearchAction)
.start()
How Zod types map to CLI options:
| Zod Type | CLI Representation |
|---|---|
z.string() |
--option-name <string> |
z.number() |
--option-name <number> |
z.boolean() |
--option-name / --no-option-name |
z.record() |
--option-name <json> |
.default(value) |
Sets the default in help text |
.describe('...') |
Sets the option description |
Field names are automatically converted to kebab-case for flags. Action names become kebab-case subcommands.
Example output:
$ mytool greet --name "World" --enthusiastic
◇ mytool - greet
ℹ HELLO, WORLD!!!
Edge Adapter
Deploy your actions as a stateless MCP server on any Web-Standard edge/serverless runtime - Cloudflare Workers, Vercel, Bun, Deno, Hono. Each request creates a fresh server instance - no sessions, no persistent connections, fully compatible with serverless constraints.
// api/mcp.ts (edge / serverless function)
import { silkweave } from '@silkweave/core'
import { edge } from '@silkweave/edge'
import { SearchAction } from '../actions/search.js'
const { adapter, handler } = edge()
await silkweave({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
.adapter(adapter)
.action(SearchAction)
.start()
export default { fetch: handler }
The edge() function returns a compound object - adapter wires into the Silkweave builder, while handler/GET/POST/DELETE are the request handler for your route.
For Next.js App Router (app/api/mcp/route.ts):
const { adapter, GET, POST, DELETE } = edge()
await silkweave({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
.adapter(adapter)
.action(SearchAction)
.start()
export { GET, POST, DELETE }
How it works:
- Uses
WebStandardStreamableHTTPServerTransportfrom the MCP SDK in stateless mode (sessionIdGenerator: undefined) - Each request creates a fresh
McpServer+ transport, registers tools, handles the request, and returns a Web StandardResponse - Only
POSTcarries JSON-RPC;GET(standing SSE stream) andDELETE(session teardown) return405, since stateless mode has no session to attach a stream to or tear down (aGETstream would otherwise hang the request on serverless runtimes) - Logging goes to
process.stderr(Vercel log drain) and MCP client notifications - No CORS handling - use Next.js middleware or
vercel.jsonheaders
EdgeAdapterOptions:
| Option | Type | Default | Description |
|---|---|---|---|
enableJsonResponse |
boolean |
false |
Return JSON instead of SSE streams |
auth |
AuthConfig |
- | Bearer-token validation + OAuth routes |
path |
string |
/mcp |
The MCP transport path |
Because it's a Web Standard (Request) => Response handler, the same edge() adapter runs unchanged on Cloudflare Workers, Vercel, Bun, Deno, and Hono. The examples/cloudflare example is a full Worker deployment - stateless MCP + Google Workspace OAuth 2.1 with OAuth state in Cloudflare KV (using createRedisStore over a KV adapter, since Workers have no filesystem) - with a from-scratch Cloudflare + Google setup guide in its README.
Framework Integrations
Beyond the transport adapters, Silkweave meets your framework where it is.
NestJS
@silkweave/nestjs exposes your existing NestJS controllers as MCP tools - additively. Add @Mcp() to a route handler and its name, description, and input schema are reflected from the route, the @Param/@Query/@Body decorators, and any @nestjs/swagger / class-validator metadata the method already carries. On a tool call the validated input is split back into the method's positional arguments and the handler runs directly, with @UseGuards() applied first.
@Controller('users')
export class UsersController {
@Get(':id')
@ApiParam({ name: 'id', description: 'User ID' })
@Mcp() // -> MCP tool "UsersGet", input { id: string }
get(@Param('id') id: string) {
return this.users.find(id)
}
}
@Module({
imports: [SilkweaveModule.forRoot({
silkweave: { name: 'my-app', description: 'My app', version: '1.0.0' },
adapters: [mcp({ basePath: '/mcp' })]
})],
controllers: [UsersController]
})
export class AppModule {}
Controllers keep serving HTTP unchanged; removing @Mcp() fully reverts a method.
Next.js
@silkweave/nextjs projects one action set onto Next.js App Router route handlers - MCP tools for agents and a typed tRPC endpoint for your frontend - from a single source of truth. It's action-first and additive (it only adds route files), wrapping @silkweave/edge and @silkweave/trpc with catch-all path normalization and end-to-end tRPC types. No next/react dependency.
// silkweave/server.ts
import { defineSilkweave } from '@silkweave/nextjs'
import { banUser, listUsers } from './actions'
export const app = defineSilkweave({
name: 'my-app', description: 'My app', version: '1.0.0',
actions: [listUsers, banUser]
})
export type AppRouter = typeof app.Router
// app/api/mcp/[[...mcp]]/route.ts - one catch-all serves transport + OAuth + well-known
export const { GET, POST, DELETE, OPTIONS } = app.mcp({ basePath: '/api/mcp' })
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
// app/api/trpc/[trpc]/route.ts
export const { GET, POST, OPTIONS } = app.trpc({ endpoint: '/api/trpc' })
Vercel AI SDK
@silkweave/ai bridges the Vercel AI SDK's useChat hook to a Silkweave streaming action over a tRPC subscription - no /api/chat route, no Data Stream Protocol parsing. createChatAction() wraps streamText() into a streaming action; silkweaveTransport() is a custom ChatTransport that turns a subscribe-style function into the ReadableStream<UIMessageChunk> useChat consumes directly.
Agent Skills over MCP
Silkweave servers can serve Agent Skills - the SKILL.md directories Claude Code and other agents use - versioned, digest-verified, and installable with one command. This is what the silkweave npm package is: the CLI that installs and updates skills from any Silkweave (or SEP-2640-conforming) MCP server.
import { defineSkill, silkweave } from '@silkweave/core'
import { http } from '@silkweave/mcp/server'
await silkweave({ name: 'team-skills', description: 'Team skills server', version: '1.0.0' })
.adapter(http({
host: 'localhost',
port: 8080,
skills: [
defineSkill({ dir: './skills/commit-message', npmPackage: 'commit-message-skill' }),
defineSkill({ dir: './skills/release-checklist', tags: ['internal'] })
],
skillsMarketplace: { owner: { name: 'Your Team' } } // optional: Claude Code plugin marketplace
}))
.start()
# every machine on the team converges on one server
npx silkweave skills sync --url https://skills.example.com/mcp --token $TOKEN
- Serving - the
skillsoption (onstdio(),http(),mcpTransport(),edge()) serves each skill three ways at once:skill://file resources (the SEP-2640 baseline),ListSkills/GetSkilltools (work with every MCP client today), and a server-instructions pointer that activates the skills in hosts. An experimentalskillsExtension: trueadditionally serves the draft SEP-2640skills/list/skills/getmethods. - Installing -
silkweave skills sync|install|list|outdated|pin|unpinmaintain a lockfile with per-file sha256 digests; every install is re-verified client-side, so a server can never write outside the target directory. - Private and mixed sharing - put the server behind bearer/OAuth auth; a per-request
filterActionsthat hides the skill tools also hides the resources and instructions. - Public skills -
silkweave skills packwraps a skill into a skills-only Claude Code plugin package fornpm publish; theskillsMarketplaceoption serves/.claude-plugin/marketplace.jsonso users get native/plugin install+/plugin update.
See @silkweave/skills (serving) and the silkweave CLI (installing, packing, universal MCP proxy).
Authentication
@silkweave/auth is split into two layers so a pure resource server never pulls the OAuth issuer machinery into its graph:
- Root (
@silkweave/auth) - the spec-required resource-server core (jose-only): validate bearer tokens, delegate issuance to an external IdP. @silkweave/auth/oauth- the opt-in authorization-server layer: front your own OAuth 2.1 flow, with persistence stores.
AuthConfig is accepted by the MCP http(), edge(), tRPC, and framework adapters.
Resource-Server Core
The root export is the minimal, spec-required core: bearer-token validation (expiry + issuer binding per RFC 9207, audience binding per RFC 8707, step-up scope challenge per SEP-2350) plus protected-resource metadata (RFC 9728, including scopes_supported). It is jose-only - importing it never pulls the OAuth issuer machinery.
import { edge } from '@silkweave/edge'
import type { AuthConfig } from '@silkweave/auth'
const auth: AuthConfig = {
// validate the bearer token against your external IdP
verifyToken: async (token) => {
const claims = await validateWithIdp(token)
return { token, clientId: claims.sub, scopes: claims.scope?.split(' ') ?? [] }
}
}
const { adapter, handler } = edge({ auth })
Opt-in OAuth 2.1
To front your own OAuth 2.1 flow, import from the @silkweave/auth/oauth subpath. This is where the authorization-server proxy lives - PKCE, refresh tokens, client ID metadata documents (CIMD), and dynamic client registration - along with the persistence stores (memory, JSON file, and Redis). Providers like google() and the createRedisStore / createJsonStore factories all come from this subpath.
import { edge } from '@silkweave/edge'
import { google, createJsonStore } from '@silkweave/auth/oauth'
const auth = google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
store: createJsonStore('./oauth-state.json')
})
const { adapter, handler } = edge({ auth })
Importing only @silkweave/auth keeps the issuer/store machinery out of your bundle; reach for @silkweave/auth/oauth only when you run the OAuth 2.1 flow yourself.
Logging and Progress
Every action receives a context.logger with eight severity levels plus a progress reporter:
run: async (input, { logger }) => {
logger.debug('Starting operation...')
logger.info('Processing item')
logger.warning('Rate limit approaching')
logger.error('Failed to connect')
// Progress reporting (renders as MCP progress notifications,
// Fastify trace logs, or console output depending on adapter)
for (let i = 1; i <= total; i++) {
logger.progress({
progress: i,
total: total,
message: `Processing item ${i} of ${total}`
})
await processItem(i)
}
return { processed: total }
}
How logging is handled per adapter:
| Level | MCP (stdio/http) | Fastify | CLI |
|---|---|---|---|
debug |
notifications/message |
logger.debug() |
log.message() |
info |
notifications/message |
logger.info() |
log.info() |
warning |
notifications/message |
logger.warn() |
log.warn() |
error |
notifications/message |
logger.error() |
log.error() |
critical |
notifications/message |
logger.fatal() |
log.error() |
progress |
notifications/progress |
logger.trace() |
console.info() |
Advanced Patterns
Multiple Adapters Simultaneously
Run an MCP server and a REST API from the same set of actions:
import { silkweave } from '@silkweave/core'
import { stdio } from '@silkweave/mcp'
import { fastify } from '@silkweave/fastify'
await silkweave({ name: 'my-platform', description: 'Multi-transport', version: '1.0.0' })
.adapter(stdio())
.adapter(fastify({ host: 'localhost', port: 8080, logger: true }))
.action(SearchAction)
.action(GreetAction)
.action(AnalyzeAction)
.start()
All adapters start concurrently. The MCP stdio server communicates over stdin/stdout while Fastify listens on port 8080 - each serving the exact same actions.
CLI Arguments vs Options
By default, all Zod fields become CLI --options. Use the args property to promote fields to positional arguments:
export const DeployAction = createAction({
name: 'deploy',
description: 'Deploy to an environment',
input: z.object({
environment: z.string().describe('Target environment'),
tag: z.string().describe('Release tag'),
dryRun: z.boolean().describe('Simulate without deploying').default(false)
}),
args: ['environment', 'tag'],
run: async ({ environment, tag, dryRun }, { logger }) => {
logger.info(`Deploying ${tag} to ${environment}${dryRun ? ' (dry run)' : ''}`)
// ...
return { deployed: !dryRun }
}
})
$ mytool deploy production v2.1.0 --dry-run
◇ mytool - deploy
ℹ Deploying v2.1.0 to production (dry run)
Fields listed in args become positional arguments in the CLI. All other fields remain as --options. The args property has no effect on MCP or REST adapters - they always receive all fields as a single input object.
Complex Input Types
Zod's full expressiveness is available for input schemas:
export const ImportAction = createAction({
name: 'import',
description: 'Import data from a source',
input: z.object({
source: z.string().describe('Data source URL'),
format: z.string().describe('File format').default('json'),
batchSize: z.number().int().min(1).max(10000).describe('Records per batch').default(500),
tags: z.record(z.string()).describe('Key-value metadata tags').optional(),
overwrite: z.boolean().describe('Overwrite existing records').default(false)
}),
run: async (input, { logger }) => {
logger.info(`Importing from ${input.source} in ${input.format} format`)
logger.info(`Batch size: ${input.batchSize}, overwrite: ${input.overwrite}`)
if (input.tags) {
logger.debug(`Tags: ${JSON.stringify(input.tags)}`)
}
// ...
return { imported: 1500 }
}
})
In MCP, this becomes a tool with a full JSON Schema. In the CLI, tags becomes --tags <json> accepting a JSON string. In Fastify, it's a documented POST body.
MCP Tool Quality
Four features (3.2) make the MCP surface agent-grade - all opt-in, all defined on the action or the adapter:
createAction({
name: 'users.get',
description: 'Get a user by id',
input: z.object({ id: z.string().describe('User id from UsersList') }),
output: z.object({ id: z.string(), name: z.string() }),
disposition: 'structured', // output becomes the MCP outputSchema contract
annotations: { idempotentHint: true }, // merged over the kind-derived readOnlyHint
tags: ['users', 'read'], // matched by filterActions below
kind: 'query',
run: async ({ id }) => getUser(id)
})
http({
host: 'localhost', port: 8080,
filterActions: async (actions, request) => scopeToApiKey(actions, request), // per-request tool list
onToolCall: (event) => log('tool_call', event) // fire-and-forget telemetry
})
- Tool annotations - every tool ships
ToolAnnotations(readOnlyHintderived fromkind; explicit fields win), which MCP hosts use to group and permission-gate tools. - Structured output (
disposition: 'structured') - theoutputschema is visible intools/listbefore the call and the schema-parsed result ships asstructuredContent(extra fields stripped; mismatches degrade to agent-legible tool errors). Note the default result format is now compact JSON -disposition: 'smart'opts back into embedded-resource sideloading for large payloads. - Per-request tool filtering (
filterActions) - the stateless transports recompute the tool list per request, so per-API-key permissions are one callback; a thrownSilkweaveErrorsurfaces as itsstatusCode, never an empty tool list. - Telemetry (
onToolCall) - one fire-and-forget event per call (durationMs,ok,resultBytes,sideloaded, resolved auth viacontext); in NestJS wired through DI (forRoot({ telemetry: MyTelemetryService })) covering MCP and tRPC.
See the @silkweave/mcp README for details.
Action Linter
Silkweave ships a dev-time guardrail that flags agent-hostile action definitions - missing or throwaway description text and undescribed input params - the cheap mistakes that quietly degrade an agent's tool use.
silkweave().start() runs the linter automatically, emitting warnings to stderr via console.warn (safe for stdio servers). Disable it with SilkweaveOptions.lint: false:
await silkweave({ name: 'my-tools', description: 'My Tools', version: '1.0.0', lint: false })
.adapter(stdio())
.action(MyAction)
.start()
You can also run it directly via lintActions(actions) (returns the findings) or reportActionLint(actions) (prints them), both exported from @silkweave/core.
MCP Client Configuration
Claude Desktop
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["/absolute/path/to/server.js"]
}
}
}
Claude Code
Add to .mcp.json in your project root:
{
"mcpServers": {
"my-tools": {
"command": "pnpm",
"args": ["tsx", "server.ts"]
}
}
}
MCP Inspector
For debugging with the MCP Inspector:
{
"mcpServers": {
"my-tools": {
"command": "pnpm",
"args": ["tsx", "server.ts"]
}
}
}
npx @modelcontextprotocol/inspector --config .mcp.json --server my-tools
HTTP Transport
For the streamable HTTP adapter, point your client at the /mcp endpoint:
http://localhost:8080/mcp
API Reference
silkweave(options)
Creates a new Silkweave builder instance.
import { silkweave } from '@silkweave/core'
function silkweave(options: SilkweaveOptions): Silkweave
interface SilkweaveOptions {
name: string // Server/app name
description: string // Human-readable description
version: string // Semantic version
}
interface Silkweave {
adapter(generator: AdapterGenerator): Silkweave // Add an adapter
action(action: Action): Silkweave // Mount an action
actions(actions: Action[]): Silkweave // Mount multiple actions
start(): Promise<Silkweave> // Start all adapters
}
createAction(action)
Type-safe action factory. Returns the action object as-is with full type inference.
import { createAction } from '@silkweave/core'
function createAction<I extends object, O extends object>(
action: Action<I, O>
): Action<I, O>
interface Action<I, O> {
name: string
description: string
input: z.ZodType<I> & { shape: Record<string, z.ZodTypeAny> }
output?: z.ZodType<O> // used by typegen + tRPC
kind?: 'query' | 'mutation' // tRPC dispatch (default 'mutation')
args?: (keyof I)[] // CLI positional args
isEnabled?: (co
No comments yet
Be the first to share your take.