Server helpers reference
@chataway/apps/server — a thin layer over the official MCP SDK: tools checked against your manifest, file params, returning files, result cards, account tokens and elicitation.
Your app's tools live on an ordinary MCP server that speaks Streamable HTTP. You can write it with any MCP library in any language. If you use Node, @chataway/apps/server takes care of the Chataway-specific parts on top of the official SDK.
npm install @chataway/apps @modelcontextprotocol/sdk zodimport { createAppServer, fileParam, returnFile, card } from '@chataway/apps/server';
import manifest from '../plugin.json' with { type: 'json' };
const app = createAppServer({ manifest });
app.tool('make_stickers', { input: { images: fileParam({ multiple: true }) } }, async ({ images }) => ({
text: `Made ${images.length} sticker(s).`,
files: images.map(f => returnFile(toSticker(f.bytes), 'image/png', f.name)),
card: card.gallery({ title: 'Your stickers' }),
}));
await app.listen({ port: 8787 }); // http://localhost:8787/mcpWhat the helpers guarantee — and what they don't:
- Manifest and server stay in sync.
app.tool()throws for a tool id thatplugin.jsondoesn't declare, and when file params differ between the manifest and your input shape.listen()refuses to start while a declared tool has no implementation. - Files arrive decoded, and are checked a second time against
accept/maxBytes. - The real guarantees — only declared tools are exposed, only consented files arrive,
confirmtools run only after approval — are enforced by Chataway, not by this library.
#createAppServer(options)
| Option | Type | |
|---|---|---|
manifest | object | string | Your parsed plugin.json, or a path to it. Validated at startup (dev level). Required. |
name, version | string | Reported to MCP clients. Default: manifest id / version. |
instructions | string | Optional MCP server instructions. |
log | (line) => void | false | Log lines (default console.error with your app id). |
Returns an AppServer.
#app.tool(id, definition, handler)
Implements a tool declared in plugin.json.
app.tool('search_assets', {
input: {
query: z.string().describe('What to look for'),
limit: z.number().int().min(1).max(50).default(10),
},
annotations: { readOnlyHint: true },
}, async ({ query, limit }, ctx) => {
const results = await api.search(query, limit, ctx.requireAccount());
return { json: { results } };
});definition | |
|---|---|
input | A zod raw shape of the arguments. Use fileParam() for every param under the tool's files. |
title | Default: the manifest's name. |
annotations | MCP tool annotations: readOnlyHint, destructiveHint, idempotentHint, openWorldHint. |
The description always comes from plugin.json — it's what was reviewed.
#Handler arguments
handler(args, ctx). args are your parsed arguments, with file params decoded to AppFile (or AppFile[] for multiple, AppFile | undefined when optional).
ctx | |
|---|---|
token | The connected account's bearer token, or null. |
requireAccount() | The token, or throws AccountRequiredError. |
chataway | { project, chat, app } — the X-Chataway-Project / -Chat pseudonyms, shape-checked (app is null: Chataway doesn't send an app header). Not proof of origin. |
headers | Raw request headers. |
signal | An AbortSignal, aborted when Chataway cancels the call (the user stopped it, or the 120 s limit). |
elicit(message, schema) | Ask the user mid-call; becomes a form card. Resolves { action: 'accept' | 'decline' | 'cancel', content? }. |
progress(progress, total?, message?) | Send an MCP progress notification (when the caller asked for progress). |
extra | The MCP SDK's raw handler extra. |
#Return values
Return any of:
- a string → text for the agent,
- the shorthand
{ text?, json?, files?, card?, isError? }, - a full MCP
CallToolResult({ content, structuredContent?, _meta?, isError? }).
| Shorthand | Becomes |
|---|---|
text | A text content item. |
json | A text item with the JSON, plus structuredContent when it's an object. |
files | returnFile(...) items, appended to content. |
card | _meta["chataway/card"]. |
isError | isError: true. |
A thrown error becomes { isError: true, content: [{ type: 'text', text: message }] } — write messages the agent can act on.
#Files
#fileParam(options?)
A zod schema for a file argument. On the wire it's an MCP embedded resource; your handler gets an AppFile.
| Option | ||
|---|---|---|
multiple | boolean | A list of files. Must match "multiple" in plugin.json. |
optional | boolean | The argument may be left out. |
description | string | Schema description. |
interface AppFile {
name: string; // file name
mimeType: string;
bytes: Uint8Array;
size: number;
uri: string; // the opaque chataway-file:// URI
}#returnFile(bytes, mimeType, name)
An embedded-resource content item for a file you return. With files:return, Chataway keeps it in your app's storage, shows it to the user, and gives the agent a ref. bytes: Uint8Array, ArrayBuffer or Buffer.
#decodeFile(value) · mimeAccepted(mime, accept)
Low-level: decode one embedded resource into an AppFile; test a MIME type against an accept list (image/*).
#Cards
import { card, withCard } from '@chataway/apps/server';
return { text: 'Deployed.', card: card.notice({ title: 'Preview is live', body: 'https://preview.example.com/p/42' }) };
return withCard(existingResult, card.gallery({ title: 'Top matches', body: 'Licensed for commercial use.' }));card.notice({ title, body? }) | A Markdown notice. |
card.gallery({ title, body?, items? }) | Titles the gallery of files this result returned. (items isn't rendered by Chataway yet.) |
withCard(result, card) | Add a card to a CallToolResult. |
cardMeta(card) | { "chataway/card": card }, to spread into _meta. |
See Cards & approvals.
#Accounts and headers
requireAccount(req) | The bearer token from anything with headers (a Node/Express request, a Fetch Request, an MCP extra), or throws AccountRequiredError. |
AccountRequiredError | Throw it when there's no token or your API rejects it; the agent gets your message as an error. |
verifyChatawayHeaders(req, { require? }) | Parse and shape-check X-Chataway-Project and -Chat. Throws on malformed values (and on missing ones with require: true). |
readHeader(req, name) | Case-insensitive header lookup. |
The pseudonyms are not proof that a request came from Chataway — anyone can send headers. Authenticate with the account token, and treat pseudonyms as untrusted partition keys.
#Serving
#app.listen(options?)
A small built-in HTTP server. Returns { url, port, server, close() }.
| Option | Default | |
|---|---|---|
port | $PORT or 8787 | |
host | 127.0.0.1 | Use 0.0.0.0 behind your load balancer. |
path | /mcp | The MCP endpoint. |
routes | — | (req, res) => boolean | Promise<boolean> for your own API routes (for example, uploads from your panel). Return true when handled. |
It also serves GET /healthz → { ok, app, version, tools }.
In production, terminate TLS in front of it (a load balancer, a platform's HTTPS) — the store requires an https mcpUrl.
#app.handle(req, res, parsedBody?) — Express and friends
Mount the MCP endpoint in your existing server instead:
import express from 'express';
const web = express();
web.use(express.json({ limit: '60mb' })); // file params are base64 in the body
web.post('/chataway/mcp', (req, res) => app.handle(req, res, req.body));
web.listen(3000);The server is stateless (a fresh MCP server per POST; no session ids), so it scales horizontally without sticky sessions. Only POST is accepted.
#app.createMcpServer() · app.check()
Low-level: a fresh McpServer with every tool registered (for your own transport), and the "every declared tool is implemented" check listen() runs.
#Without the helpers
Any MCP server works. What Chataway sends and expects:
File params arrive as embedded resources in the argument's place (a list when multiple):
{ "type": "resource", "resource": { "uri": "chataway-file://a1b2c3/photo.png", "mimeType": "image/png", "blob": "<base64>" } }Returned files are content items: image, audio, an embedded resource with a blob, or a resource_link on your remote.domains.
Result cards go in _meta["chataway/card"]: { "kind": "notice", "title", "body" }, or { "kind": "gallery", "title", "body" } to title the gallery of returned files.
Headers: X-Chataway-Project on every request, plus X-Chataway-Chat and Authorization: Bearer … (tools with account) on tool calls.
Elicitation: standard elicitation/create with a flat requestedSchema.
With the official TypeScript SDK, a minimal stateless server:
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
function buildServer() {
const mcp = new McpServer({ name: 'my-app', version: '1.0.0' });
mcp.registerTool('hello', {
description: 'Greets someone.', // Chataway shows the manifest's description instead
inputSchema: { name: z.string() },
}, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }));
return mcp;
}
const web = express();
web.use(express.json({ limit: '60mb' }));
web.post('/mcp', async (req, res) => {
const mcp = buildServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on('close', () => { transport.close(); mcp.close(); });
await mcp.connect(transport);
await transport.handleRequest(req, res, req.body);
});
web.listen(8787);