Chataway Developers
Reference

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.

bash
npm install @chataway/apps @modelcontextprotocol/sdk zod
server/index.ts
ts
import { 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/mcp

What the helpers guarantee — and what they don't:

  • Manifest and server stay in sync. app.tool() throws for a tool id that plugin.json doesn'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, confirm tools run only after approval — are enforced by Chataway, not by this library.

#createAppServer(options)

OptionType
manifestobject | stringYour parsed plugin.json, or a path to it. Validated at startup (dev level). Required.
name, versionstringReported to MCP clients. Default: manifest id / version.
instructionsstringOptional MCP server instructions.
log(line) => void | falseLog lines (default console.error with your app id).

Returns an AppServer.

#app.tool(id, definition, handler)

Implements a tool declared in plugin.json.

ts
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
inputA zod raw shape of the arguments. Use fileParam() for every param under the tool's files.
titleDefault: the manifest's name.
annotationsMCP 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
tokenThe 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.
headersRaw request headers.
signalAn 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).
extraThe 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? }).
ShorthandBecomes
textA text content item.
jsonA text item with the JSON, plus structuredContent when it's an object.
filesreturnFile(...) items, appended to content.
card_meta["chataway/card"].
isErrorisError: 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
multiplebooleanA list of files. Must match "multiple" in plugin.json.
optionalbooleanThe argument may be left out.
descriptionstringSchema description.
ts
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

ts
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.
AccountRequiredErrorThrow 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() }.

OptionDefault
port$PORT or 8787
host127.0.0.1Use 0.0.0.0 behind your load balancer.
path/mcpThe 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:

ts
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):

json
{ "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:

ts
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);