# Mcpfusion Development

> How to build production MCP servers with MCP Fusion using the MVA (Model-View-Agent) pattern. Use this skill whenever writing, modifying, or reviewing MCP Fusion code — including tools, Presenters, Models, middleware, prompts, routers, tests, or server configuration. Activate even when the user just says "create a tool", "add an endpoint", "write a Presenter", or mentions @mcpfusion/core, defineModel, definePresenter, initMCPFusion, FluentToolBuilder, or any MCP Fusion API. This skill covers the entire framework surface.

- Skill: `vinkius-labs/mcpfusion-development` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds add vinkius-labs/mcpfusion-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vinkius-labs/mcpfusion-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: Apache-2.0
- Author: vinkius-labs (https://skillmd.com/u/vinkius-labs)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/vinkius-labs/mcpfusion-development

---


# mcpfusion development Guide

MCP Fusion is a TypeScript framework for MCP servers built on the **MVA (Model-View-Agent)** pattern. The Model validates data, the Presenter (View) shapes what the AI perceives, and Tools (Agent layer) wire it all together.

> For the complete API reference with all type signatures, read [llms.txt](../../../llms.txt) at the root of the repository. This skill covers the essential patterns and rules.

## Reference Examples

Complete, runnable examples are available in `references/`. Read them for concrete implementation patterns:

| Example | Domain | Patterns Shown |
|---|---|---|
| [example-complete-crud.ts](references/example-complete-crud.ts) | Product Catalog | Full MVA lifecycle: Model → Presenter → Router → Query/Mutation/Action, ErrorBuilder, State Sync |
| [example-proxy-api.ts](references/example-proxy-api.ts) | Blog Platform | `.proxy()` API pass-through, `.fromModel()`, field aliases, path params (`:id`), `.handle()` vs `.proxy()` decision tree |
| [example-server-setup.ts](references/example-server-setup.ts) | Generic App | Full server bootstrap: context, `initMCPFusion()`, middleware, `autoDiscover()`, prompts, State Sync policies, `startServer()` |
| [example-testing.ts](references/example-testing.ts) | Customer Service | `@mcpfusion/testing`: Egress Firewall assertions, JIT System Rules, RBAC middleware, error handling, Symbol Invisibility |

## Project Structure

```
src/
├── models/               ← M — defineModel() declarations
│   ├── InvoiceModel.ts
│   └── UserModel.ts
├── views/                ← V — Presenters
│   ├── invoice.presenter.ts
│   └── user.presenter.ts
├── agents/               ← A — Tool definitions
│   ├── billing.tool.ts
│   └── users.tool.ts
├── index.ts              ← ToolRegistry + registerAll()
└── server.ts             ← attachToServer() bootstrap
```

**Layer import rule:** `agents/` → `views/` → `models/` → `@mcpfusion/core`. Never import backwards.

## The Golden Rules

1. **ALWAYS use `defineModel()` for domain entity schemas** — never raw `z.object()`. Models go in `models/`.
2. **Presenters receive Models via `.schema(MyModel)`** — the Presenter is the egress firewall.
3. **`with*()` methods are for tool INPUT parameters only** (filters, IDs, pagination) — NOT for domain schemas.
4. **Handlers return raw data** — the framework wraps with `success()` automatically. No boilerplate.
5. **One Model + one Presenter per entity**, reused across every tool and prompt.
6. **Use semantic verbs**: `f.query()` = readOnly, `f.mutation()` = destructive, `f.action()` = neutral.

## defineModel() — The "M" in MVA

Every domain entity starts here. Produces a `Model` with a compiled Zod `.schema`.

```typescript
import { defineModel } from '@mcpfusion/core';

export const InvoiceModel = defineModel('Invoice', m => {
    m.casts({
        id:           m.string(),
        amount_cents: m.number('CRITICAL: in CENTS. Divide by 100 for display.'),
        status:       m.enum('Status', ['paid', 'pending', 'overdue']),
        client_name:  m.string('Client name'),
    });
});

export const UserModel = defineModel('User', m => {
    m.casts({
        id:    m.string(),
        name:  m.string('Full name'),
        email: m.string('Email address'),
        role:  m.enum('Role', ['admin', 'member', 'guest']),
    });
    m.hidden(['password_hash', 'stripe_token']);  // Never exposed
    m.timestamps();                                // created_at + updated_at
    m.fillable({
        create: ['name', 'email', 'role'],
        update: ['name', 'email'],
    });
});
```

### Type Helpers

| Method | Produces | Use |
|---|---|---|
| `m.string(label?)` | `z.string()` | General text |
| `m.text(label?)` | `z.string()` | Markdown / long content |
| `m.number(label?)` | `z.number()` | Numeric |
| `m.boolean(label?)` | `z.boolean()` | Flags |
| `m.date(label?)` | `z.string()` | YYYY-MM-DD |
| `m.timestamp(label?)` | `z.string()` | ISO datetime |
| `m.uuid(label?)` | `z.string()` | UUID |
| `m.id(label?)` | `z.number()` | Always required |
| `m.enum(label, values)` | `z.enum()` | Valid values |

### FieldDef Chaining

```typescript
m.enum('Status', ['open', 'done']).default('open')
m.string('Display name').alias('displayName')     // agent says 'name', API gets 'displayName'
m.number('Score').examples([85, 92, 100])
```

### Model.toApi() — Alias Resolution

Strips undefined values and renames aliased fields. Used automatically by `.proxy()`, call explicitly in `.handle()`:

```typescript
const data = TaskModel.toApi(input);
// { title: 'X', body: 'Y' }  ← alias applied, undefined stripped
```

## Presenter — The "V" in MVA

The Presenter is the egress firewall between your handler and the wire. Schema MUST come from `defineModel()`.

### definePresenter() — Object Config (Recommended)

```typescript
import { definePresenter, ui } from '@mcpfusion/core';

export const InvoicePresenter = definePresenter({
    name: 'Invoice',
    schema: InvoiceModel,                  // ← Model, never z.object()
    // autoRules: true (default) — .describe() annotations become system rules
    ui: (inv) => [ui.echarts({ series: [{ type: 'gauge', data: [{ value: inv.amount_cents / 100 }] }] })],
    agentLimit: { max: 50, onTruncate: (n) => ui.summary({ omitted: n, hint: 'Use filters.' }) },
    suggestActions: (inv) => inv.status === 'pending'
        ? [{ tool: 'billing.pay', reason: 'Process payment', args: { id: inv.id } }]
        : [],
    embeds: [{ key: 'client', presenter: ClientPresenter }],
});
```

### createPresenter() — Fluent Builder

```typescript
import { createPresenter, ui } from '@mcpfusion/core';

const UserPresenter = createPresenter('User')
    .schema(UserModel)
    .systemRules(['Display name in bold'])
    .uiBlocks((user) => [ui.summary({ total: 1, showing: 1 })])
    .agentLimit(50, { warningMessage: 'Showing {shown} of {total}. Use filters.' })
    .suggestActions((user) => [
        { tool: 'users.update', reason: 'Edit this user', args: { id: user.id } },
    ])
    .embed('team', TeamPresenter);
```

### Presenter Layers

| Layer | What It Does |
|---|---|
| Egress Firewall | `.parse()` strips undeclared fields — PII never reaches the wire |
| JIT System Rules | Rules travel with data, not in the global prompt |
| Server-Rendered UI | ECharts, Mermaid — deterministic, no hallucinated charts |
| Cognitive Guardrails | `.agentLimit()` truncates + injects guidance |
| Action Affordances | `.suggestActions()` — HATEOAS for agents |
| Relational Composition | `.embed()` — child Presenters inherit the full pipeline |
| Prompt Bridge | `PromptMessage.fromView()` — same source of truth for tools AND prompts |

## Fluent API — Tools

### Semantic Verbs

```typescript
import { initMCPFusion } from '@mcpfusion/core';

interface AppContext { db: PrismaClient; user: { id: string; role: string } }
export const f = initMCPFusion<AppContext>();

// f.query()    — readOnly: true   (GET, no side effects)
// f.mutation() — destructive: true (DELETE, irreversible)
// f.action()   — neutral          (PUT/POST, no assumptions)
```

### Building a Tool

```typescript
export default f.query('billing.get_invoice')
    .describe('Get an invoice by ID')
    .withString('id', 'The invoice ID')
    .returns(InvoicePresenter)
    .handle(async (input, ctx) => {
        // input.id: string ✅ — fully typed, zero manual interfaces
        return await ctx.db.invoices.findUnique({ where: { id: input.id } });
        // ↑ Return raw data — Presenter handles validation, stripping, rendering
    });
```

### with*() Type-Chaining

Each call narrows the TypeScript generic — `input` is fully typed in `.handle()`:

| Method | Adds |
|---|---|
| `.withString(name, desc?)` | `Record<K, string>` |
| `.withOptionalString(name, desc?)` | `Partial<Record<K, string>>` |
| `.withNumber(name, desc?)` | `Record<K, number>` |
| `.withOptionalNumber(name, desc?)` | `Partial<Record<K, number>>` |
| `.withBoolean(name, desc?)` | `Record<K, boolean>` |
| `.withEnum(name, values, desc?)` | `Record<K, V>` (literal union) |
| `.withArray(name, itemType, desc?)` | `Record<K, T[]>` |

**Bulk variants** reduce verbosity — `.withStrings({...})`, `.withOptionalStrings({...})`, etc.:

```typescript
f.query('tasks.filter')
    .withStrings({
        company_slug: 'Workspace identifier',
        project_slug: 'Project identifier',
    })
    .withOptionalStrings({
        title: 'Filter by title',
        workflow: 'Column name',
    })
    .withOptionalNumbers({ per_page: 'Results per page' })
```

### .proxy() — Zero-Boilerplate API Proxying

Terminal method (alternative to `.handle()`) — auto-generates a handler that proxies to `ctx.client`:

```typescript
// HTTP method inferred from verb: query → GET, mutation → POST, action → PUT
// Path params resolved from input: ':id' consumes input.id
// Auto-unwraps { data: ... } envelopes
f.query('user.get')
    .withString('id', 'User UUID')
    .proxy('users/:id');  // → GET users/abc-123
```

**When to use:** `.proxy()` for simple pass-through, `.handle()` when you need business logic.

### .fromModel() — Model-Driven Input

Imports fillable fields from a Model's profile — zero manual `.with*()` calls:

```typescript
const createTask = f.mutation('tasks.create')
    .fromModel(TaskModel, 'create')  // imports title, description, status
    .proxy('tasks');

const updateTask = f.action('tasks.update')
    .fromModel(TaskModel, 'update')
    .handle(async (input, ctx) => {
        const data = TaskModel.toApi(input);  // alias applied, undefined stripped
        await ctx.client.put(`tasks/${input.id}`, data);
    });
```

**Decision tree:**
- **Simple CRUD, no logic** → `.fromModel()` + `.proxy()`
- **Custom logic** → `.fromModel()` + `.handle()` + `Model.toApi()`

### .instructions() — AI-First Guidance

```typescript
f.query('docs.search')
    .describe('Search internal documentation')
    .instructions('Use ONLY when the user asks about internal policies. Do NOT use for general questions.')
```

Injected as `[INSTRUCTIONS]` in the tool description — reduces hallucination.

## FluentRouter — Prefix Grouping

Shares prefix, middleware, and tags across child tools:

```typescript
const users = f.router('users')
    .describe('User management')
    .use(requireAuth)
    .tags('core');

const listUsers = users.query('list')
    .withOptionalNumber('limit', 'Max results')
    .handle(async (input, ctx) => ctx.db.users.findMany({ take: input.limit }));

const deleteUser = users.mutation('delete')
    .withString('id', 'User ID')
    .handle(async (input, ctx) => ctx.db.users.delete({ where: { id: input.id } }));
```

## Middleware

tRPC-style context derivation — enriches `ctx` type for `.handle()`:

```typescript
const requireAuth = f.middleware(async (ctx) => {
    const user = await db.getUser(ctx.token);
    if (!user) throw new Error('Unauthorized');
    return { user, permissions: user.permissions };
});

// ctx.user and ctx.permissions — fully typed downstream
f.mutation('admin.action')
    .use(requireAuth)
    .handle(async (input, ctx) => {
        ctx.user;        // ← typed!
        ctx.permissions; // ← typed!
    });
```

## ErrorBuilder — Self-Healing Errors

```typescript
const project = await ctx.db.projects.findUnique({ where: { id: input.id } });
if (!project) {
    return f.error('NOT_FOUND', `Project "${input.id}" not found`)
        .suggest('Check the ID. Use projects.list to see valid IDs.')
        .actions('projects.list', 'projects.search')
        .details({ searched_id: input.id })
        .retryAfter(0);
}
```

## State Sync

Prevents temporal blindness — the agent knows when cached data is stale:

```typescript
f.query('geo.countries').cached();                    // immutable — cache forever
f.query('billing.balance').stale();                   // volatile — never cache
f.mutation('sprints.update').invalidates('sprints.*', 'tasks.*');  // causal invalidation
```

## Prompts — The Presenter Bridge

```typescript
import { definePrompt, PromptMessage } from '@mcpfusion/core';

const AuditPrompt = definePrompt<AppContext>('audit', {
    args: { invoiceId: 'string' } as const,
    handler: async (ctx, { invoiceId }) => {
        const invoice = await ctx.db.getInvoice(invoiceId);
        return {
            messages: [
                PromptMessage.system('You are a Senior Financial Auditor.'),
                ...PromptMessage.fromView(InvoicePresenter.make(invoice, ctx)),
                PromptMessage.user('Begin the audit.'),
            ],
        };
    },
});
```

`PromptMessage.fromView()` decomposes a Presenter into XML-tagged messages — same schema, rules, and affordances in both tools and prompts.

## Testing with @mcpfusion/testing

Runs the REAL execution pipeline in RAM — zero tokens consumed, deterministic:

```typescript
import { createMCPFusionTester } from '@mcpfusion/testing';

const tester = createMCPFusionTester(registry, {
    contextFactory: () => ({ prisma: mockPrisma, tenantId: 't_42', role: 'ADMIN' }),
});

// Egress Firewall — PII physically absent
const result = await tester.callAction('db_user', 'find_many', { take: 5 });
expect(result.data[0]).not.toHaveProperty('passwordHash');

// JIT System Rules — travel with data
expect(result.systemRules).toContain('Email addresses are PII.');

// Middleware — GUEST blocked
const denied = await tester.callAction('db_user', 'find_many', { take: 5 }, { role: 'GUEST' });
expect(denied.isError).toBe(true);
```

Assert every MVA layer: `result.data`, `result.systemRules`, `result.uiBlocks`, `result.isError`.

## Common Anti-Patterns — What NOT to Do

### ❌ Using raw z.object() for domain schemas

```typescript
// WRONG:
const presenter = definePresenter({
    name: 'User',
    schema: z.object({ id: z.string(), name: z.string() }),  // ← NO!
});

// CORRECT:
const UserModel = defineModel('User', m => {
    m.casts({ id: m.string(), name: m.string() });
});
const presenter = definePresenter({ name: 'User', schema: UserModel });
```

### ❌ Manual success() wrapping

```typescript
// WRONG:
.handle(async (input, ctx) => {
    return success(await ctx.db.users.findMany());  // ← unnecessary
});

// CORRECT:
.handle(async (input, ctx) => {
    return await ctx.db.users.findMany();  // ← framework wraps automatically
});
```

### ❌ Manual if-checks for optional fields

```typescript
// WRONG:
.handle(async (input, ctx) => {
    const data: Record<string, unknown> = {};
    if (input.title) data.title = input.title;
    if (input.color) data.color = input.color;
    await ctx.client.updateItem(input.id, data);
});

// CORRECT:
.handle(async (input, ctx) => {
    const data = ItemModel.toApi(input);  // strips undefined, applies aliases
    await ctx.client.updateItem(input.id, data);
});
```

### ❌ Importing backwards between layers

```typescript
// WRONG: model importing from tool
// models/UserModel.ts
import { listUsers } from '../agents/users.tool';  // ← NO!

// CORRECT: agents → views → models → @mcpfusion/core (one direction only)
```

### ❌ Duplicating Presenter logic across tools

```typescript
// WRONG: formatting response manually in each handler
// CORRECT: define Presenter ONCE in views/, use .returns(Presenter) in every tool
```

### ❌ Using .withString() for domain entity fields

```typescript
// WRONG: defining domain fields as tool input params
f.mutation('users.create')
    .withString('name', 'User name')
    .withString('email', 'Email')        // ← These are domain fields!
    .withString('role', 'User role')

// CORRECT: use .fromModel() for domain fields
f.mutation('users.create')
    .fromModel(UserModel, 'create')      // ← imports from Model's fillable profile
```

## Quick Reference

| I want to... | Use |
|---|---|
| Define a domain entity | `defineModel('Name', m => { ... })` |
| Shape what the AI sees | `definePresenter({ schema: MyModel, ... })` |
| Create a read tool | `f.query('entity.list')` |
| Create a write tool | `f.mutation('entity.delete')` |
| Create an update tool | `f.action('entity.update')` |
| Group related tools | `f.router('prefix')` |
| Add auth middleware | `.use(requireAuth)` |
| Return self-healing errors | `f.error('CODE', 'message').suggest(...)` |
| Proxy to an API | `.proxy('endpoint/:id')` |
| Import fields from Model | `.fromModel(MyModel, 'profile')` |
| Test MVA pipeline | `createMCPFusionTester(registry, { contextFactory })` |
| Control caching | `.cached()`, `.stale()`, `.invalidates()` |
| Build a prompt | `definePrompt('name', { handler })` |
| Bridge Presenter to prompt | `PromptMessage.fromView(presenter.make(data, ctx))` |

