| name | afd-typescript |
| description | TypeScript implementation patterns for AFD commands using Zod schemas, @lushly-dev/afd-server, and @lushly-dev/afd-core. Covers command definition, schema design, error handling, MCP server setup, embeddable Node handlers, and testing. Use when: implementing commands in TypeScript, setting up MCP servers, writing Zod schemas, or debugging TypeScript AFD code. Triggers: typescript afd, ts command, zod schema, defineCommand, createMcpServer, createMcpHandler, @lushly-dev/afd-server, @lushly-dev/afd-core, typescript implementation.
|
AFD TypeScript Implementation
Patterns for implementing AFD commands in TypeScript.
Parity Rule
TypeScript often serves as the reference implementation for the shared AFD surface, but parity SHOULD still be defined by framework-agnostic capabilities and agent-visible behavior rather than literal module symmetry.
- Core AFD surfaces MUST stay framework-agnostic.
- React or browser integrations SHOULD stay in examples or ecosystem layers.
- Cross-language implementations MAY use idiomatic APIs when they preserve the same command contract and behavior.
- Shared parity features to keep aligned include output schemas, validated examples, prerequisite metadata, context scoping, grouped/lazy discovery strategies, and the
afd-call / afd-batch / afd-pipe / afd-discover / afd-detail tool family.
Package Imports
import type { CommandResult, CommandError } from '@lushly-dev/afd-core';
import {
defineCommand,
success,
error,
createMcpServer,
createMcpHandler,
} from '@lushly-dev/afd-server';
import { z } from 'zod';
import { ViewStateRegistry, createViewStateCommands } from '@lushly-dev/afd-view-state';
Command Definition
Basic Command
import { z } from 'zod';
import { defineCommand, success, error } from '@lushly-dev/afd-server';
const inputSchema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
const Todo = z.object({
id: z.string(),
title: z.string(),
priority: z.enum(['low', 'medium', 'high']),
completed: z.boolean(),
});
export const createTodo = defineCommand({
name: 'todo-create',
description: 'Create a new todo item',
category: 'todo',
mutation: true,
requires: ['auth-sign-in'],
version: '1.0.0',
input: inputSchema,
output: Todo,
contexts: ['task-management'],
errors: ['VALIDATION_ERROR'],
async handler(input) {
const parsed = inputSchema.parse(input);
const todo = await store.create(parsed);
return success(todo, {
reasoning: `Created todo "${todo.title}" with ${parsed.priority} priority`,
confidence: 1.0,
});
},
});
Command with Context
export const updateTodo = defineCommand({
name: 'todo-update',
description: 'Update a todo item',
category: 'todo',
mutation: true,
input: updateSchema,
errors: ['NOT_FOUND', 'NO_CHANGES'],
async handler(input, context) {
console.log(`[${context.traceId}] Updating todo ${input.id}`);
const todo = await store.get(input.id);
if (!todo) {
return error('NOT_FOUND', `Todo ${input.id} not found`, {
suggestion: 'Use todo-list to see available todos',
});
}
},
});
Zod Schema Patterns
Basic Types
const schema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
description: z.string().max(1000).optional(),
count: z.number().int().positive().max(100),
completed: z.boolean().default(false),
id: z.string().uuid(),
email: z.string().email(),
dueDate: z.string().datetime().optional(),
});
Arrays and Nested Objects
const schema = z.object({
tags: z.array(z.string()).max(10).default([]),
items: z.array(z.object({
name: z.string(),
quantity: z.number().int().positive(),
})),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/),
}).optional(),
});
Refinements and Transforms
const dateRangeSchema = z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}).refine(
(data) => new Date(data.endDate) > new Date(data.startDate),
{ message: 'End date must be after start date' }
);
const normalizedSchema = z.object({
email: z.string().email().transform(e => e.toLowerCase()),
name: z.string().transform(n => n.trim()),
});
Union Types (Ordering Matters!)
const TokenValueSchema = z.union([
z.string(),
z.object({ web: z.string(), ios: z.string() }).strict(),
z.record(z.string(), z.unknown()),
]);
Success Responses
return success(todo);
return success(todo, {
reasoning: `Created todo "${todo.title}"`,
});
return success(suggestion, {
reasoning: 'Generated based on user history',
confidence: 0.85,
});
return success(result, {
reasoning: 'Deleted 5 items',
warnings: [
{ code: 'PERMANENT', message: 'This action cannot be undone' },
],
});
return success(user, {
reasoning: 'User created successfully',
suggestions: ['Add profile photo', 'Set notification preferences'],
});
Error Responses
return error('NOT_FOUND', `Todo ${input.id} not found`, {
suggestion: 'Use todo-list to see available todos',
});
return error('VALIDATION_ERROR', 'Title cannot be empty', {
suggestion: 'Provide a title between 1 and 200 characters',
});
return error('FORBIDDEN', 'You cannot modify this resource', {
suggestion: 'Contact the owner to request access',
});
return error('CONFLICT', 'Email already registered', {
suggestion: 'Use user.login instead, or reset password',
});
return error('NO_CHANGES', 'No fields to update', {
suggestion: 'Provide at least one field to update',
});
MCP Server Setup
Basic Server
import { createMcpServer } from '@lushly-dev/afd-server';
import { allCommands } from './commands/index.js';
const server = createMcpServer({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
});
await server.start();
console.log(`MCP server running at ${server.getUrl()}`);
Embeddable Node Handler
Use createMcpHandler() when a framework or platform owns the HTTP server lifecycle and expects a Node request handler:
import { createServer } from 'node:http';
import { createMcpHandler } from '@lushly-dev/afd-server';
import { allCommands } from './commands/index.js';
const handler = createMcpHandler({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
host: '127.0.0.1',
port: 3100,
});
createServer((req, res) => {
void handler(req, res);
}).listen(3100, '127.0.0.1');
Use createMcpServer() for the batteries-included standalone server. Use createMcpHandler() when you need AFD to plug into an existing Node HTTP host.
Lazy Strategy (Large Command Sets)
const server = createMcpServer({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
toolStrategy: 'lazy',
});
The expected agent workflow in lazy mode is afd-discover -> afd-detail -> afd-call. afd-call, afd-batch, and afd-pipe remain available across all tool strategies; lazy just keeps discovery constant-cost for large command sets.
With Contexts
const server = createMcpServer({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
contexts: [
{ name: 'editing', description: 'Document editing tools' },
{ name: 'reviewing', description: 'Review and approval tools' },
],
});
With Middleware
import {
createMcpServer,
defaultMiddleware,
createRateLimitMiddleware,
} from '@lushly-dev/afd-server';
const server = createMcpServer({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
middleware: [
...defaultMiddleware(),
createRateLimitMiddleware({ maxRequests: 100, windowMs: 60000 }),
],
});
const server2 = createMcpServer({
name: 'my-app',
version: '1.0.0',
commands: allCommands,
middleware: defaultMiddleware({
logging: { logInput: true },
timing: { slowThreshold: 500 },
}),
});
Command Registry Pattern
export const createTodo = defineCommand({...});
export const listTodos = defineCommand({...});
import { createTodo } from './create.js';
import { listTodos } from './list.js';
import { getTodo } from './get.js';
export { createTodo, listTodos, getTodo };
export const allCommands = [
createTodo,
listTodos,
getTodo,
];
Testing Commands
Unit Tests
import { describe, it, expect, beforeEach } from 'vitest';
import { store } from '../store/memory.js';
import { createTodo } from './create.js';
beforeEach(() => {
store.clear();
});
describe('todo-create', () => {
it('creates todo with required fields', async () => {
const result = await createTodo.handler(
{ title: 'Test', priority: 'medium' },
{}
);
expect(result.success).toBe(true);
expect(result.data?.title).toBe('Test');
expect(result.reasoning).toBeDefined();
});
it('uses default priority', async () => {
const result = await createTodo.handler(
{ title: 'Test' },
{}
);
expect(result.data?.priority).toBe('medium');
});
});
AFD Compliance Tests
describe('AFD Compliance', () => {
it('success results include reasoning', async () => {
const result = await createTodo.handler({ title: 'Test' }, {});
expect(result.success).toBe(true);
expect(result.reasoning).toBeDefined();
expect(typeof result.reasoning).toBe('string');
});
it('error results include suggestion', async () => {
const result = await getTodo.handler({ id: 'nonexistent' }, {});
expect(result.success).toBe(false);
expect(result.error?.suggestion).toBeDefined();
});
});
Performance Tests
describe('Performance', () => {
it('todo-create < 10ms', async () => {
const start = performance.now();
await createTodo.handler({ title: 'Test' }, {});
const duration = performance.now() - start;
expect(duration).toBeLessThan(10);
});
});
TypeScript Gotchas
Zod Input vs Output Types
const schema = z.object({
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
async handler(rawInput: z.input<typeof schema>) {
const input = schema.parse(rawInput);
}
Generic Registry Types
class CommandRegistry {
private commands = new Map<string, CommandDefinition<any, any>>();
register<TSchema extends z.ZodType, TOutput>(
command: CommandDefinition<TSchema, TOutput>
) {
this.commands.set(command.name, command as CommandDefinition<any, any>);
}
async execute<TOutput>(name: string, input: unknown): Promise<CommandResult<TOutput>> {
const command = this.commands.get(name);
return command.handler(input) as CommandResult<TOutput>;
}
}
Project Configuration
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
biome.json (Linting)
{
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
},
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error"
},
"style": {
"useImportType": "error",
"useConst": "error"
},
"suspicious": {
"noExplicitAny": "error"
}
}
}
}
Related Skills
afd-developer - Core AFD methodology
afd-python - Python implementation patterns
afd-rust - Rust implementation patterns