@mindstudio-ai/remy 0.1.256 → 0.1.258

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -85,9 +85,23 @@ const { key, url } = await platform.upload(token, file, { onProgress: (f) => set
85
85
 
86
86
  ## Public assets + image resizing
87
87
 
88
- Public files are world-readable, on the app's domain, and **images resize via query params**
89
- (`?w=&h=&fit=&crop=&fm=&dpr=&q=&blur=&sharpen=` — same vocabulary as the image CDN; set `dpr=2/3` for
90
- retina). Request the size you need rather than CSS-scaling a full-res original.
88
+ Public files are world-readable, served on the app's own domain, and **images resize via query
89
+ params** — request the size you need rather than CSS-scaling a full-res original. Always set `dpr=2`
90
+ or `3` when sizing so images stay crisp on Retina displays.
91
+
92
+ | Param | Example | Effect |
93
+ |-------|---------|--------|
94
+ | `w` | `?w=400` | Max width in pixels |
95
+ | `h` | `?h=300` | Max height in pixels |
96
+ | `fit` | `?fit=crop` | Resize mode: `scale-down`, `contain`, `cover`, `crop`, `pad` |
97
+ | `crop` | `?crop=face` | Face-aware crop (with `fit=crop`) |
98
+ | `fm` | `?fm=webp` | Output format: `avif`, `webp`, `jpeg`, `auto` |
99
+ | `dpr` | `?dpr=2` | Device pixel ratio |
100
+ | `q` | `?q=80` | Quality (1–100) |
101
+ | `blur` | `?blur=10` | Blur radius |
102
+ | `sharpen` | `?sharpen=1` | Sharpen amount |
103
+
104
+ Combine freely: `…/hero.jpg?w=200&h=200&fit=crop&fm=avif`.
91
105
 
92
106
  Lightweight-config pattern: a public store + a stable `key` is a file the frontend can `fetch` with no
93
107
  DB hit and the backend can overwrite (`Config.put(json, { key: 'config/latest.json' })`).
@@ -4,6 +4,8 @@ Interfaces are projections of the backend contract into different modalities. Th
4
4
 
5
5
  All external service connections (webhook secrets, email addresses) are configured at the project level by the user through the Remy platform. The agent's job is to write the config files and the methods that handle the requests — not to manage API keys, OAuth flows, or service registration.
6
6
 
7
+ `{app-host}`, where it appears in a URL below, means any host the app is served on: its `custom_subdomain` host (e.g. `myapp.madewithremy.com`), a custom domain if one is configured, or the UUID host (`<appId>.madewithremy.com` / `.msagent.ai`).
8
+
7
9
  ## Web Interface
8
10
 
9
11
  A full web application — typically Vite + React, but any framework that produces static output works.
@@ -59,19 +61,16 @@ const api = createClient<{
59
61
  const { vendorId } = await api.submitVendorRequest({ name: 'Acme' });
60
62
  const { vendors } = await api.listVendors();
61
63
 
62
- // File upload (returns CDN URL)
63
- const url = await platform.uploadFile(file);
64
-
65
- // With progress tracking
66
- const url = await platform.uploadFile(file, {
67
- onProgress: (fraction) => setProgress(fraction), // 0 to 1
68
- });
64
+ // File upload → client-direct to the app's file store (see Files & Storage).
65
+ // A backend method mints an upload token; the browser uploads straight to storage.
66
+ const token = await api.getUploadSlot({ filename: file.name, contentType: file.type });
67
+ const { key, url } = await platform.upload(token, file);
69
68
 
70
- // With abort support
69
+ // With progress + abort
71
70
  const controller = new AbortController();
72
- const url = await platform.uploadFile(file, {
71
+ const { url } = await platform.upload(token, file, {
73
72
  signal: controller.signal,
74
- onProgress: (f) => setProgress(f),
73
+ onProgress: (fraction) => setProgress(fraction), // 0 to 1
75
74
  });
76
75
  controller.abort(); // cancels the upload
77
76
 
@@ -87,7 +86,7 @@ auth.verifySmsCode(verId, code) // → AppUser (sets session)
87
86
  auth.logout() // clears session
88
87
  ```
89
88
 
90
- For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. See the "Building Agent Interfaces" section for usage details.
89
+ For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. Load the `agentInterfaces` skill for its usage — thread APIs, streaming callbacks, and attachments are all there.
91
90
 
92
91
  The project uses `"jsx": "react-jsx"` (automatic JSX transform) — do not `import React from 'react'`. Only import the specific hooks and types you need (e.g., `import { useState, useEffect } from 'react'`).
93
92
 
@@ -141,494 +140,49 @@ await prerender.invalidate(['/u/abc']); // omit arg to purge all
141
140
 
142
141
  REST endpoints for external consumers — other services, mobile apps, integrations. This is separate from the web frontend's internal RPC (`@mindstudio-ai/interface` calls `/_/methods` directly and does not use the API interface). The API interface lives at `/_/api/` and exposes only the methods you choose to route.
143
142
 
144
- Use it for receiving webhooks (Stripe, Twilio), sync endpoints for other services, a public REST API, batch tools — anything where something outside the app's own frontend needs to call a method over HTTP.
145
-
146
- ### Spec: `src/interfaces/api.md`
147
-
148
- The human-readable spec. Frontmatter declares the API name and description; the body maps methods to REST routes using MSFM.
149
-
150
- ```yaml
151
- ---
152
- name: Vendor Management API
153
- description: API for managing vendors and purchase orders.
154
- type: interface/api
155
- ---
156
- ```
157
-
158
- Routes are declared as `VERB /path → methodExportName` under resource headings, with annotations for params and descriptions:
159
-
160
- ```markdown
161
- ## Vendors
162
-
163
- ### List vendors
164
- GET /vendors → listVendors
165
- ~~~
166
- Returns all vendors, optionally filtered by status.
167
- query: status (string, optional) — filter by vendor status
168
- ~~~
169
-
170
- ### Create vendor
171
- POST /vendors → submitVendorRequest
172
- ~~~
173
- Submit a new vendor for approval.
174
- body: name (string, required) — vendor name
175
- contactEmail (string, required) — billing contact
176
- ~~~
177
-
178
- ### Delete vendor
179
- DELETE /vendors/:vendorId → deleteVendor
180
- ~~~
181
- path: vendorId (string, required) — the vendor's unique identifier
182
- ~~~
183
- ```
184
-
185
- ### Compiled Output: `dist/interfaces/api/api.json`
186
-
187
- ```json
188
- {
189
- "api": {
190
- "name": "Vendor Management API",
191
- "description": "API for managing vendors and purchase orders.",
192
- "routes": [
193
- {
194
- "method": "GET",
195
- "path": "/vendors",
196
- "handler": "list-vendors",
197
- "summary": "List vendors",
198
- "description": "Returns all vendors, optionally filtered by status.",
199
- "tag": "Vendors",
200
- "params": {
201
- "query": {
202
- "status": { "type": "string", "required": false, "description": "Filter by vendor status" }
203
- }
204
- }
205
- },
206
- {
207
- "method": "POST",
208
- "path": "/vendors",
209
- "handler": "submit-vendor-request",
210
- "summary": "Create vendor",
211
- "description": "Submit a new vendor for approval.",
212
- "tag": "Vendors",
213
- "params": {
214
- "body": {
215
- "name": { "type": "string", "required": true, "description": "Vendor name" },
216
- "contactEmail": { "type": "string", "required": true, "description": "Billing contact" }
217
- }
218
- }
219
- },
220
- {
221
- "method": "DELETE",
222
- "path": "/vendors/:vendorId",
223
- "handler": "delete-vendor",
224
- "summary": "Delete vendor",
225
- "description": "Permanently remove a vendor.",
226
- "tag": "Vendors",
227
- "params": {
228
- "path": {
229
- "vendorId": { "type": "string", "required": true, "description": "The vendor's unique identifier" }
230
- }
231
- }
232
- }
233
- ]
234
- }
235
- }
236
- ```
237
-
238
- | Field | Description |
239
- |-------|-------------|
240
- | `name` | API display name (used in generated OpenAPI spec) |
241
- | `description` | API description |
242
- | `routes[].method` | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
243
- | `routes[].path` | URL path with `:param` placeholders for path params |
244
- | `routes[].handler` | Method `id` from the manifest (kebab-case) |
245
- | `routes[].summary` | Short description for the endpoint |
246
- | `routes[].description` | Longer description |
247
- | `routes[].tag` | Resource grouping (becomes a tag in OpenAPI) |
248
- | `routes[].params` | Parameter declarations: `path`, `query`, and/or `body` objects |
143
+ Use it for sync endpoints for other services, a public REST API, batch tools — anything where something outside the app's own frontend needs to call a method over HTTP. It's also the interface that hands a method the raw HTTP request (`input._request`).
249
144
 
250
- ### Platform Behavior
251
-
252
- Routes are mounted at `/_/api{path}` (e.g. `DELETE /_/api/vendors/abc123`).
253
-
254
- - **Path params** are extracted and merged into the method's input: `/:vendorId` → `{ vendorId: "abc123" }`
255
- - **Query params** are merged into input for GET requests: `?status=approved` → `{ status: "approved" }`
256
- - **Request body** for POST/PUT/PATCH is the input directly (no `{ input: {...} }` wrapper)
257
- - **Response** is the method output directly (no `{ output: {...} }` wrapper)
258
- - **Auth** via `Authorization: Bearer sk_...` (API key resolves to a user with full RBAC)
259
- - **Streaming**: `Accept: text/event-stream` header returns SSE chunks
260
- - **Raw request context**: Every API method receives `input._request` with `{ method, headers, rawBody }`. `rawBody` is the original unparsed body as a UTF-8 string — critical for webhook signature verification (Stripe, GitHub, Shopify). For most methods you don't need `_request` at all.
261
-
262
- ### Manifest
263
-
264
- ```json
265
- { "type": "api", "path": "dist/interfaces/api/api.json" }
266
- ```
145
+ **Load the `restApi` skill** before authoring `src/interfaces/api.md` or writing the config — the route spec format, param declarations, auth, and platform behaviour are all there.
267
146
 
268
147
  ## Platform-Triggered Interfaces
269
148
 
270
149
  Cron, Webhook, and Email interfaces are invoked by the platform, not by a user session. Methods called through these interfaces run with `auth.roles: ['system']`. Use `auth.requireRole('system')` to restrict a method to platform triggers only.
271
150
 
272
- ## Cron
273
-
274
- Scheduled method execution.
151
+ Each has its own skill carrying the config shape, the input the method receives, and the platform's behaviour: `scheduledJobs`, `webhooks`, `inboundEmail`.
275
152
 
276
- ### Config (`interface.json`)
153
+ ## Cron
277
154
 
278
- ```json
279
- {
280
- "cron": {
281
- "jobs": [
282
- {
283
- "schedule": "0 9 * * 5",
284
- "method": "process-weekly-payments",
285
- "description": "Process approved invoices every Friday at 9am"
286
- },
287
- {
288
- "schedule": "*/30 * * * *",
289
- "method": "sync-vendor-status",
290
- "description": "Sync vendor statuses every 30 minutes"
291
- }
292
- ]
293
- }
294
- }
295
- ```
155
+ Scheduled method execution — a method plus a cron expression, synced to the platform on deploy.
296
156
 
297
- Standard cron expression format. Jobs are synced to the platform on deploy.
157
+ **Load the `scheduledJobs` skill** before adding one.
298
158
 
299
159
  ## Webhook
300
160
 
301
161
  Inbound HTTP endpoints that invoke a method directly and synchronously — the caller waits for the method to finish. Use for receiving webhooks from external services (Stripe, GitHub, Shopify, Slack, Twilio). Direct inbound webhooks with signature verification work natively; do **not** build confirmation-token or polling workarounds.
302
162
 
303
- ### Config (`interface.json`)
304
-
305
- The top-level key must match the interface type (`webhook`):
306
-
307
- ```json
308
- {
309
- "webhook": {
310
- "endpoints": [
311
- {
312
- "method": "handle-payment-webhook",
313
- "secret": "whsec_pick_a_long_random_token",
314
- "description": "Stripe events"
315
- }
316
- ]
317
- }
318
- }
319
- ```
320
-
321
- - `method` — the id of a method in `methods[]` to invoke.
322
- - `secret` — a developer-chosen opaque token that is **both the routing key and the access guard**. It is stable across deploys (compilation is a passthrough — redeploying never rotates it), so a URL you register with Stripe/GitHub stays valid. Generate one long random value per endpoint and keep it constant.
323
- - Declare multiple endpoints if needed; each `secret` maps to one method.
324
-
325
- ### Endpoint URL
326
-
327
- Register this with the external service: `https://{app-host}/_/webhook/{secret}` — `{app-host}` is any host the app is served on: its `custom_subdomain` host (e.g. `myapp.madewithremy.com`), a custom domain if configured, or the UUID host (`<appId>.madewithremy.com` / `.msagent.ai`). All HTTP verbs are accepted.
328
-
329
- ### Input
330
-
331
- The method receives:
163
+ Routing is by a secret in the URL rather than an auth header, which is what makes it the right fit for provider callbacks — they can't send a bearer token. The API interface is the alternative when the caller can.
332
164
 
333
- ```ts
334
- {
335
- method: string; // HTTP method
336
- headers: Record<string, string>; // request headers
337
- query: Record<string, string>; // query params
338
- body: any; // parsed JSON / form body
339
- rawBody: string; // exact raw request bytes (UTF-8), pre-parse
340
- }
341
- ```
342
-
343
- For signature verification **always use `rawBody`, never `body`** — providers (Stripe, GitHub, Shopify, Slack) HMAC the raw payload, and a re-serialized `body` will not match. E.g. `stripe.webhooks.constructEvent(input.rawBody, input.headers['stripe-signature'], endpointSecret)`. `rawBody` is populated for `application/json` and `application/x-www-form-urlencoded` bodies (what these providers send).
344
-
345
- ### Response
346
-
347
- Whatever the method returns as output is sent back to the caller as JSON; if it returns no output, the platform responds `204`. A wrong/unknown secret returns `401`; an app with no live release returns `404`.
165
+ **Load the `webhooks` skill** before adding one — the secret semantics, endpoint URL, input shape, and signature verification are all there.
348
166
 
349
167
  ## Email
350
168
 
351
- Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method.
352
-
353
- ### Address tiers
354
-
355
- Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart registration); the legacy tier is specific-localpart and frozen for new apps.
356
-
357
- | Tier | Address | How it's set up |
358
- |---|---|---|
359
- | Platform subdomain (default) | `*@<custom_subdomain>.madewithremy.com` | Automatic the moment the app has a `custom_subdomain` set. Every address on that subdomain delivers to the handler. |
360
- | Custom domain | `*@<their-domain>` | The user adds a domain in the dashboard's email-domains settings and points one MX record at `mx.msagent.ai`. Not something the agent provisions. |
361
- | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
362
-
363
- Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`).
364
-
365
- A verified custom domain (and the app's `madewithremy.com` subdomain) also **sends** outbound mail, not just receives — `sendEmail` picks the app's own-brand sender automatically, configured in the dashboard's **Email** settings.
366
-
367
- ### Config (`interface.json`)
368
-
369
- ```json
370
- {
371
- "email": {
372
- "method": "handle-inbound-email",
373
- "approvedSenders": ["billing@vendor.com", "*@trusted-partner.com"]
374
- }
375
- }
376
- ```
377
-
378
- `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list applies uniformly across all three address tiers.
379
-
380
- ### Input shape
381
-
382
- ```ts
383
- {
384
- to: string; // full recipient address; localpart is arbitrary on catchall tiers
385
- from: string; // bare sender address, extracted from "Name <a@b>" form
386
- fromName: string | null; // sender display name, or null
387
- subject: string; // 'No Subject' if missing
388
- message: string; // plain-text body, falls back to HTML if text is missing; 'No Body' if neither was sent
389
- html: string; // HTML body, or '' when text-only
390
- attachments: string[]; // CDN URLs — already uploaded by the platform
391
- messageId: string | null; // this email's Message-ID, angle-bracketed (<id@host>)
392
- inReplyTo: string | null; // Message-ID this email is replying to, if any
393
- references: string[]; // prior Message-IDs in the thread (angle-bracketed); [] if none
394
- replyTo: string | null; // Reply-To address — reply here, not `from`, when set
395
- cc: string[]; // Cc recipient addresses
396
- date: string | null; // original send time, ISO-8601
397
- }
398
- ```
399
-
400
- To reply in-thread, feed these into `sendEmail`: set `inReplyTo` to the incoming `messageId` and `references` to `[...references, messageId]`. Send to `replyTo` when it's set, otherwise `from`. `sendEmail` returns `{ recipients, cc, bcc, from }` (who it sent to + the sender used); it does not return the sent message's own `Message-ID`, so thread off *inbound* mail, not off messages you sent.
401
-
402
- ### Attachments and size limits
403
-
404
- `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files. Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or downstream services.
405
-
406
- Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by the platform before the method runs.
169
+ Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method. Addresses on the app's subdomain are catchall, so per-purpose addresses (`support@`, `receipts@`) work without registering anything.
407
170
 
408
- ### Auth
409
-
410
- Methods invoked through this interface run with `auth.roles: ['system']` (see the system-roles section above). They have no user session and can't impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable via email.
171
+ **Load the `inboundEmail` skill** before writing the handler — address tiers, `approvedSenders`, the input shape, in-thread replies, and attachments are all there.
411
172
 
412
173
  ## MCP (Model Context Protocol)
413
174
 
414
175
  Expose the app to *external* AI agents — Claude Desktop, Cursor, other people's agents, anything that speaks MCP. Unlike the agent interface (which *is* an agent — its own LLM, personality, and chat UI), MCP has no model of its own; it's the app projected as an MCP server for an outside AI to drive.
415
176
 
416
- It supports the full MCP surface:
417
- - **Tools** — methods the agent can call (rich descriptions + machine-readable annotations).
418
- - **Resources** — read-only app data the agent can pull into context, addressable by URI.
419
- - **Prompts** — reusable, parameterized prompt templates the server offers.
420
- - **Instructions** — server-level guidance shown to the calling agent (the toolset's "system prompt").
421
-
422
- The platform hosts the server, handles auth like the API interface (optional — keyed or anonymous), and derives every tool's input schema from the method contract. Because the consumer is an external agent with no knowledge of your app, **the descriptions are the product** — see "Building MCP Interfaces" for how to write them.
423
-
424
- ### Spec: `src/interfaces/mcp.md`
425
-
426
- Frontmatter declares the server. The body's intro prose becomes the server `instructions`; `## Tools`, `## Resources`, and `## Prompts` sections declare the rest.
427
-
428
- ```yaml
429
- ---
430
- name: Vendor Management
431
- description: Tools and data for managing vendors and purchase orders.
432
- type: interface/mcp
433
- ---
434
- ```
435
-
436
- ```markdown
437
- This server manages vendors and purchase orders. Read a vendor before updating it; submitted
438
- requests go through approval before they become active.
439
-
440
- ## Tools
441
-
442
- ### Submit a vendor request
443
- method: submit-vendor-request
444
- ~~~
445
- Submit a new vendor for approval. Use when the caller wants to add a vendor.
446
- Do NOT use to modify an existing vendor — that's update-vendor.
447
- - name: the vendor's legal name
448
- - contactEmail: billing contact; required for approval routing
449
- Returns the created vendor's id and its initial "pending" status.
450
- ~~~
451
-
452
- ### List vendors
453
- method: list-vendors
454
- annotations: readOnly
455
- ~~~
456
- List all vendors, newest first. Read-only.
457
- ~~~
458
-
459
- ## Resources
460
-
461
- - list-vendors → app://vendors — "Vendors" — all vendors (application/json)
462
- - get-vendor → app://vendors/{id} — "Vendor" — a single vendor by id (application/json)
463
-
464
- ## Prompts
465
-
466
- ### draft_vendor_email
467
- description: Draft an outreach email to a vendor.
468
- arguments: vendorId (required) — the vendor to contact
469
- ~~~
470
- Write a warm outreach email to vendor {{vendorId}} introducing our procurement process.
471
- ~~~
472
- ```
473
-
474
- Don't hand-author input schemas — the platform derives them. For a resource template, `{param}` in the URI maps to the backing method's input.
475
-
476
- ### Compiled Output: `dist/interfaces/mcp/`
477
-
478
- ```
479
- dist/interfaces/mcp/
480
- ├── interface.json ← config the platform reads
481
- ├── instructions.md ← server-level guidance (returned in `initialize`)
482
- ├── tools/
483
- │ ├── submitVendorRequest.md ← rich description, one per tool
484
- │ └── listVendors.md
485
- └── prompts/
486
- └── draftVendorEmail.md ← prompt template body, one per prompt
487
- ```
488
-
489
- Resources carry inline metadata only — no per-resource file.
490
-
491
- ### Config (`interface.json`)
177
+ It supports the full MCP surface: tools (methods the agent can call), resources (read-only app data addressable by URI), prompts (parameterized templates), and instructions (server-level guidance for the whole toolset). The platform hosts the server, handles auth, and derives every tool's input schema from the method contract.
492
178
 
493
- ```json
494
- {
495
- "mcp": {
496
- "name": "Vendor Management",
497
- "description": "Tools and data for managing vendors and purchase orders.",
498
- "instructions": "instructions.md",
499
- "tools": [
500
- {
501
- "method": "submit-vendor-request",
502
- "name": "submit_vendor_request",
503
- "title": "Submit Vendor Request",
504
- "description": "tools/submitVendorRequest.md",
505
- "annotations": { "readOnly": false, "destructive": false, "idempotent": false, "openWorld": false }
506
- },
507
- {
508
- "method": "list-vendors",
509
- "title": "List Vendors",
510
- "description": "tools/listVendors.md",
511
- "annotations": { "readOnly": true }
512
- }
513
- ],
514
- "resources": [
515
- { "method": "list-vendors", "uri": "app://vendors", "name": "Vendors", "description": "All vendors.", "mimeType": "application/json" },
516
- { "method": "get-vendor", "uriTemplate": "app://vendors/{id}", "name": "Vendor", "description": "A single vendor by id.", "mimeType": "application/json" }
517
- ],
518
- "prompts": [
519
- {
520
- "name": "draft_vendor_email",
521
- "title": "Draft vendor email",
522
- "description": "Draft an outreach email to a vendor.",
523
- "arguments": [ { "name": "vendorId", "description": "The vendor to contact", "required": true } ],
524
- "template": "prompts/draftVendorEmail.md"
525
- }
526
- ]
527
- }
528
- }
529
- ```
530
-
531
- | Field | Description |
532
- |-------|-------------|
533
- | `name`, `description` | Server display name + registry metadata (not shown to the calling agent) |
534
- | `instructions` | Relative path to the server-level guidance returned in `initialize` |
535
- | `tools[].method` | Method `id` from the manifest (kebab-case) |
536
- | `tools[].name` | Tool name exposed to clients. Optional — defaults to the method `id`. Must match `[a-zA-Z0-9_-]` and be unique within the server |
537
- | `tools[].title` | Optional human-friendly display name |
538
- | `tools[].description` | Relative path to the tool's markdown description |
539
- | `tools[].annotations` | Optional client hints (auto-call vs. confirm): `readOnly`, `destructive`, `idempotent`, `openWorld` — map to MCP's `readOnlyHint` etc. |
540
- | `resources[].method` | The read method invoked when the resource is read |
541
- | `resources[].uri` / `uriTemplate` | A static URI, or a template whose `{param}` maps to the method's input |
542
- | `resources[].name`, `description`, `mimeType` | Resource metadata |
543
- | `prompts[].name`, `title`, `description` | Prompt identity + metadata |
544
- | `prompts[].arguments` | `[{ name, description?, required? }]` |
545
- | `prompts[].template` | Relative path to the template body (`{{arg}}` placeholders) |
546
-
547
- There is no `inputSchema` field — the platform derives each tool's schema from the method's input contract.
548
-
549
- ### Platform Behavior
550
-
551
- - The platform hosts the MCP server and exposes it to external clients. Clients connect at `POST https://{app-host}/_/mcp`.
552
- - **Auth is optional**, identical to the API interface: a `Bearer` key resolves to a user with full RBAC; with no key, calls run anonymously (no user, no roles). The method is the boundary — gate sensitive tools with `auth.requireRole`/`requireUser`; a public (keyless) server exposes only the un-gated tools.
553
- - Input schemas are derived automatically from each method's input contract.
554
- - `tools/list` is static; access is enforced per-method at call time (a gated tool is listed but rejects an unauthorized call).
555
- - A resource read invokes the backing method (template `{param}`s come from the URI) and returns its output as the resource contents.
556
- - `prompts/get` fills the template with the provided arguments.
557
- - `instructions` is returned in the `initialize` response.
558
-
559
- ### Manifest
560
-
561
- ```json
562
- { "type": "mcp", "path": "dist/interfaces/mcp/interface.json" }
563
- ```
179
+ **Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md`. Because the consumer is an external agent with no knowledge of your app, the descriptions are the product — the skill carries both how to write them and the full config contract.
564
180
 
565
181
  ## Agent (Conversational Interface)
566
182
 
567
- A conversational interface where an LLM has access to the app's methods as tools. Unlike MCP (which exposes methods for external agents), the agent interface IS the agent — it has its own personality, system prompt, and model config, and orchestrates tool calls against the app's methods internally.
568
-
569
- ### Spec: `src/interfaces/agent.md`
570
-
571
- The human-readable spec. Frontmatter contains structured fields; the prose body is the behavioral spec — voice, personality, capabilities, rules — written in MSFM.
572
-
573
- ```yaml
574
- ---
575
- name: Todo Assistant
576
- model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 16000}
577
- description: Conversational agent that helps users manage their to-do list.
578
- ---
579
- ```
580
-
581
- Frontmatter fields:
582
- - `name` — agent display name
583
- - `model` — JSON string with `model` (MindStudio model ID), `temperature`, `maxResponseTokens`, and optional `config` (model-specific settings like `reasoning`, `tools`, etc.). Use `askMindStudioSdk` to look up available model IDs and their config options when setting the model ID. The user's UI will have a nice visual picker to allow them to change it later, so only validate model when you're setting - otherwise assume this value to be correct if it changes.
584
- - `description` — one-liner for agent card/listing
585
-
586
- The prose body contains sections like Voice & Personality, Capabilities, Behavior — whatever structure serves the agent's character. This is compiled into the system prompt and tool descriptions.
183
+ A conversational interface where an LLM has access to the app's methods as tools. Unlike MCP (which exposes methods for external agents), the agent interface IS the agent — it has its own personality, system prompt, and model config, and orchestrates tool calls against the app's methods internally. Chat runs as the authenticated user, so every tool call carries that user's roles.
587
184
 
588
- ### Compiled Output: `dist/interfaces/agent/`
589
-
590
- ```
591
- dist/interfaces/agent/
592
- ├── agent.json ← config the platform reads
593
- ├── system.md ← compiled system prompt
594
- └── tools/
595
- ├── createTodo.md ← rich tool description per method
596
- ├── listTodos.md
597
- └── ...
598
- ```
599
-
600
- ### Config (`agent.json`)
601
-
602
- ```json
603
- {
604
- "agent": {
605
- "model": "claude-4-5-haiku",
606
- "temperature": 0.5,
607
- "maxTokens": 16000,
608
- "systemPrompt": "system.md",
609
- "tools": [
610
- { "method": "create-todo", "description": "tools/createTodo.md" },
611
- { "method": "list-todos", "description": "tools/listTodos.md" }
612
- ],
613
- "webInterfacePath": "/chat"
614
- }
615
- }
616
- ```
617
-
618
- | Field | Description |
619
- |-------|-------------|
620
- | `model` | MindStudio model ID (e.g. `claude-4-5-haiku`, `claude-5-sonnet`) |
621
- | `temperature` | Model temperature |
622
- | `maxTokens` | Max response tokens |
623
- | `systemPrompt` | Relative path to the compiled system prompt markdown file |
624
- | `tools` | Array of tool entries — `method` references a method `id` from the manifest, `description` is a relative path to a markdown file with rich tool docs (when to use, examples, edge cases, parameter guidance) |
625
- | `webInterfacePath` | Optional. If the app has a web interface with a chat page, this path tells the IDE where to show the preview. Otherwise the agent is accessed via API. |
626
-
627
- ### Manifest Declaration
628
-
629
- ```json
630
- { "type": "agent", "path": "dist/interfaces/agent/agent.json" }
631
- ```
185
+ **Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI — the spec frontmatter, compiled output, `agent.json`, and the entire frontend surface are all there.
632
186
 
633
187
  ## Manifest Declaration
634
188
 
@@ -638,7 +192,7 @@ Each interface is declared in `mindstudio.json`:
638
192
  {
639
193
  "interfaces": [
640
194
  { "type": "web", "path": "dist/interfaces/web/web.json" },
641
- { "type": "api" },
195
+ { "type": "api", "path": "dist/interfaces/api/api.json" },
642
196
  { "type": "cron", "path": "dist/interfaces/cron/interface.json" },
643
197
  { "type": "webhook", "path": "dist/interfaces/webhook/interface.json" },
644
198
  { "type": "email", "path": "dist/interfaces/email/interface.json" },
@@ -648,4 +202,4 @@ Each interface is declared in `mindstudio.json`:
648
202
  }
649
203
  ```
650
204
 
651
- Some interfaces (like `api`) work without a config file — just declaring the type is enough. Others need a config for command mappings, schedules, etc. Set `"enabled": false` to skip an interface during build.
205
+ An interface with nothing to configure can be declared with just its type; the rest point at a compiled config file. Set `"enabled": false` to skip an interface during build.
@@ -87,21 +87,12 @@ await mindstudio.sendEmail({
87
87
  body: content, // markdown or HTML, auto-detected; bodyType overrides. cc/bcc/replyTo/attachments also supported
88
88
  });
89
89
 
90
- // Reply in-thread to an inbound email (fields come from the email interface's input)
91
- await mindstudio.sendEmail({
92
- to: input.replyTo ?? input.from,
93
- subject: `Re: ${input.subject}`,
94
- body: reply,
95
- inReplyTo: input.messageId ?? undefined,
96
- references: input.messageId ? [...input.references, input.messageId] : input.references,
97
- cc: input.cc, // reply-all
98
- });
90
+ // Replying to inbound mail is the `inboundEmail` skill's job — load it before writing
91
+ // a handler. It has the full input shape and the threading rules; getting the headers
92
+ // wrong sends a reply that starts a new conversation instead of continuing one.
99
93
 
100
- // Upload files
101
- const { url } = await mindstudio.uploadFile({
102
- data: buffer,
103
- fileName: 'report.pdf',
104
- });
94
+ // Store a file → returns a stable URL (define the store at module scope; see Files & Storage)
95
+ const { url } = await Reports.put(buffer, { contentType: 'application/pdf', filename: 'report.pdf' });
105
96
 
106
97
  // Web scraping
107
98
  const { markdown } = await mindstudio.scrapeUrl({
@@ -221,6 +212,8 @@ export async function createPurchaseOrder(input: {
221
212
 
222
213
  A method can return immediately while kicking off slow work (like `runTask()`) that continues in the background. Don't await the slow call — use `.then()` / `.catch()` to update the record when it completes, and return an early result to the caller. The frontend polls the record's status to track progress.
223
214
 
215
+ The example below shows the fire-and-forget shape, not a complete `runTask()` call. Load the `taskAgents` skill before writing one — configuring its tools, validating the output, and handling failures are all there, and none of them are visible here.
216
+
224
217
  ```typescript
225
218
  export async function enrichRestaurant(input: { id: string; name: string }) {
226
219
  await Restaurants.update(input.id, { status: 'enriching' });
@@ -324,7 +317,9 @@ input._request: {
324
317
  }
325
318
  ```
326
319
 
327
- `rawBody` preserves the exact bytes the client sent — whitespace, key ordering, encoding. Use it for webhook signature verification:
320
+ `rawBody` preserves the exact bytes the client sent — whitespace, key ordering, encoding. Use it for signature verification.
321
+
322
+ **There are two inbound HTTP interfaces and they expose the raw body differently.** This one is the API interface, at `input._request.rawBody`. The Webhook interface puts it at top-level `input.rawBody` and routes by a secret in the URL instead of a bearer token, which usually suits provider callbacks better since a provider can't send one. Load the `webhooks` skill before choosing — the example below is the API-interface shape and won't work unchanged in a webhook handler.
328
323
 
329
324
  ```typescript
330
325
  export async function stripeWebhook(input: {