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:
{
"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:
| Field | Effect |
|---|---|
files.images | The 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 / maxBytes | Checked by Chataway before anything is sent. A PDF never reaches you; neither does a 40 MB photo. |
prepare | Chataway 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. |
confirm | A blocking approval card. Nothing leaves the Mac until the user taps Make stickers. {{count}} becomes the number of files. |
files:receive | Required for any tool with files. |
host:media | Required for prepare. |
files:return | Required 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:
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/mcpfileParam({ multiple: true })declaresimagesas 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 —createAppServerthrows at startup ifimagesisn't amultiplefile param inplugin.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.cardasks Chataway to show a card with the result (it becomes_meta["chataway/card"]). A gallery card withoutitemsshows 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:
- The agent calls
make_stickerswith refs to the photos. - 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.
- Prepare. Chataway resizes and converts each image. You'll see this in the Inspector as a host job.
- Confirm. Your card appears: "Make 2 sticker(s)?" with your body text and a Make stickers button.
- The call. Chataway sends your server the tool call with each image as an embedded resource.
- 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?"
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:
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:
{ "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, andconfirm. - 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.