Chataway Developers
Getting started

Your first tool

Build a tool end to end — it receives images from the chat, has Chataway prepare them, asks the user for approval and returns new files.

In this tutorial you'll build Sticker Maker: the user says "turn these three photos into stickers", the agent hands the photos to your tool, the user approves on a card, and your server returns die-cut sticker PNGs that land in the chat as a gallery.

Along the way you'll use the four things most tools need: a file param, prepare (a host media operation), a confirm card, and returning files.

You'll need an app from the quickstart, running under chataway dev.

#1. Declare the tool

Tools are declared in plugin.json — that's the part the store reviews and the user consents to. Add this tool, and the two grants it needs:

plugin.json
json
{
  "grants": ["files:receive", "files:return", "host:media"],
  "permissions": [
    "Receive the images you choose to turn into stickers",
    "Save the finished stickers to your Media library"
  ],
  "contributes": {
    "tools": [
      {
        "id": "make_stickers",
        "name": "Make stickers",
        "description": "Turn photos or illustrations into die-cut sticker PNGs with a white border. Pass the images in `images`. Returns one sticker per image.",
        "files": {
          "images": {
            "accept": ["image/png", "image/jpeg", "image/webp"],
            "multiple": true,
            "maxBytes": 8000000,
            "prepare": { "op": "resize", "width": 1024, "height": 1024, "fit": "contain", "format": "png" }
          }
        },
        "confirm": {
          "title": "Make {{count}} sticker(s)?",
          "body": "The images are sent to Sticker Maker's servers and deleted after processing.",
          "action": "Make stickers"
        }
      }
    ]
  }
}

What each piece does:

FieldEffect
files.imagesThe images argument carries files. The agent passes file refs (from the Media library, the project, an upload, another app); Chataway resolves them and sends your server the bytes.
accept / maxBytesChecked by Chataway before anything is sent. A PDF never reaches you; neither does a 40 MB photo.
prepareChataway resizes every image to fit 1024×1024 and converts it to PNG on the Mac, with its own code, before hand-off. You always get predictable input. maxBytes applies after prepare.
confirmA blocking approval card. Nothing leaves the Mac until the user taps Make stickers. {{count}} becomes the number of files.
files:receiveRequired for any tool with files.
host:mediaRequired for prepare.
files:returnRequired to return files that are kept in your app's storage and offered to the Media library.

Every grant must be explained in permissions — plain sentences the user reads when enabling the app.

#2. Implement it on your server

Your server registers a tool with the same id. File params arrive as MCP embedded resources; the fileParam() helper declares the argument and decodes it for you:

server/index.ts
ts
import { createAppServer, fileParam, returnFile, card } from '@chataway/apps/server';
import manifest from '../plugin.json' with { type: 'json' };
import { makeSticker } from './sticker.ts'; // your image code

const app = createAppServer({ manifest });

app.tool('make_stickers', {
  input: { images: fileParam({ multiple: true }) },
}, async ({ images }) => {
  // images: Array<{ name, mimeType, bytes, size }> — already 1024² PNGs.
  const files = [];
  for (const img of images) {
    const png = await makeSticker(img.bytes);
    files.push(returnFile(png, 'image/png', img.name.replace(/\.\w+$/, '') + '-sticker.png'));
  }
  return {
    text: `Made ${files.length} sticker(s).`,
    files,
    card: card.gallery({ title: 'Your stickers' }),
  };
});

await app.listen({ port: 8787 }); // http://localhost:8787/mcp
  • fileParam({ multiple: true }) declares images as a list of files in the tool's input schema and hands your handler decoded files: name, mimeType, bytes, size. You never get a path. It must match the manifest — createAppServer throws at startup if images isn't a multiple file param in plugin.json.
  • returnFile(bytes, mime, name) builds an MCP embedded resource. Chataway stores each returned file in your app's storage and gives the agent a file ref it can pass on — to another tool, to Save to project, anywhere.
  • card asks Chataway to show a card with the result (it becomes _meta["chataway/card"]). A gallery card without items shows the files you just returned.
  • The description comes from plugin.json, not from the code.

If you'd rather not use the helpers, see Server helpers for the raw MCP shapes — it's a few lines more.

#3. Try it

Save both files. chataway dev reloads the app; your server restarts. In a new chat, attach a couple of photos (or pick some from the Media library) and ask:

Make stickers out of these.

Here's what happens, in order:

  1. The agent calls make_stickers with refs to the photos.
  2. Hand-off consent. The first time files move from one owner to another in a project (say, from the Media library to Sticker Maker), Chataway asks: "Sticker Maker wants 2 images", with thumbnails — Allow once, Always for this project, or Deny.
  3. Prepare. Chataway resizes and converts each image. You'll see this in the Inspector as a host job.
  4. Confirm. Your card appears: "Make 2 sticker(s)?" with your body text and a Make stickers button.
  5. The call. Chataway sends your server the tool call with each image as an embedded resource.
  6. The result. Your text goes to the agent. The two PNGs are saved to your app's storage and shown as a gallery card with your icon and name. The agent can now say "Done — want me to save them to the project?"
Make 2 sticker(s)?Sticker Maker
The images are sent to Sticker Maker's servers and deleted after processing.
Make stickersCancel

If the user taps Cancel, your server is never called and the agent is told the user declined.

#4. Handle errors the agent can act on

Throwing from a handler returns an MCP error result (isError: true) with your message. Write it for the agent — it will read it and often retry or explain to the user:

ts
if (img.size === 0) {
  throw new Error(`${img.name} is empty. Ask the user for a different image.`);
}

Or return { text: '…', isError: true } when you want to control the whole result.

#5. Long jobs

A tool call may take at most 120 seconds. If sticker generation can take longer (a batch of 50), return a job id right away and add a second tool the agent can poll:

plugin.json (tools)
json
{ "id": "sticker_job_status", "name": "Check sticker job",
  "description": "Check a sticker job started by make_stickers. Returns 'running' or the finished stickers." }

The agent sees your first result — "Started job j_82f1; call sticker_job_status in a little while" — and polls. Return the files from the status tool once they're ready.

#What you built

  • A declared tool with a file param, prepare, and confirm.
  • A server handler that receives bytes and returns files.
  • A result card.

Next, read How apps run for the full lifecycle of a call, or Files & hand-off for everything about refs, consent and limits.