@mindstudio-ai/remy 0.1.257 → 0.1.259

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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Agent Interfaces
3
- what: Conversational AI as a first-class interface to the app — an LLM with authenticated, per-user access to the app's methods as tools, paired with a streaming chat UI. The platform handles auth, tool dispatch, threads, and streaming, so the work is authorship: who the agent is, which methods it can reach, and how each one is described to it. Any app whose methods do something interesting can be projected into a conversation this way, often as its most compelling surface. This reference covers both halves — writing the agent spec, and building the chat frontend with the SDK's `createAgentChatClient()`.
4
- when: Before authoring `src/interfaces/agent.md`, compiling `dist/interfaces/agent/`, or building an agent's chat UI. The `<interfaces>` platform doc has the wiring; this is how to author one well.
3
+ what: Conversational AI as a first-class interface to the app — an LLM with authenticated, per-user access to the app's methods as tools, paired with a streaming chat UI. The platform handles auth, tool dispatch, threads, and streaming, so the work is authorship: who the agent is, which methods it can reach, and how each one is described to it. Any app whose methods do something interesting can be projected into a conversation this way, often as its most compelling surface. This reference covers the whole feature — writing the agent spec, compiling it, and building the chat frontend.
4
+ when: Before authoring `src/interfaces/agent.md`, compiling `dist/interfaces/agent/`, or building an agent's chat UI.
5
5
  ---
6
6
 
7
7
  # Building Agent Interfaces
@@ -81,6 +81,8 @@ When building the `dist/interfaces/agent/`, consider the agent spec, as well as
81
81
 
82
82
  **`agent.json`** — ties it together. Model config from frontmatter, paths to system prompt and tool files, optional `webInterfacePath`.
83
83
 
84
+ The exact shape of all three, and of the spec frontmatter they compile from, is in "The wiring" at the end of this document.
85
+
84
86
  ## Chat UI Design
85
87
 
86
88
  When the agent has a web frontend (via `webInterfacePath`), the chat UI is a page within the web interface.
@@ -203,3 +205,90 @@ The chat UI uses the app's design system — colors, typography, voice from `@br
203
205
 
204
206
  - Avoid designs that look like dated messaging apps from 2015
205
207
  - Avoid robotic empty states ("Hello! I'm your AI assistant. How can I help you today?")
208
+
209
+ ---
210
+
211
+ # The wiring
212
+
213
+ ## Spec: `src/interfaces/agent.md`
214
+
215
+ The human-readable spec. Frontmatter contains structured fields; the prose body is the behavioral spec —
216
+ voice, personality, capabilities, rules — written in MSFM.
217
+
218
+ ```yaml
219
+ ---
220
+ name: Todo Assistant
221
+ model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 16000}
222
+ description: Conversational agent that helps users manage their to-do list.
223
+ ---
224
+ ```
225
+
226
+ Frontmatter fields:
227
+
228
+ - `name` — agent display name
229
+ - `model` — JSON string with `model` (MindStudio model ID), `temperature`, `maxResponseTokens`, and
230
+ optional `config` (model-specific settings like `reasoning`, `tools`, etc.). Ask `askMindStudioSdk`
231
+ for available model IDs and their config options — MindStudio's ids don't match vendor ids, so treat
232
+ any id in this document's examples as illustrative rather than current. The user's UI has a visual picker for changing it later, so only validate
233
+ the model when you're setting it; if the value changes afterwards, assume it's correct.
234
+ - `description` — one-liner for agent card/listing
235
+
236
+ The prose body contains sections like Voice & Personality, Capabilities, Behavior — whatever structure
237
+ serves the agent's character. This is compiled into the system prompt and tool descriptions.
238
+
239
+ ## Compiled Output: `dist/interfaces/agent/`
240
+
241
+ ```
242
+ dist/interfaces/agent/
243
+ ├── agent.json ← config the platform reads
244
+ ├── system.md ← compiled system prompt
245
+ └── tools/
246
+ ├── createTodo.md ← rich tool description per method
247
+ ├── listTodos.md
248
+ └── ...
249
+ ```
250
+
251
+ ## Config (`agent.json`)
252
+
253
+ ```json
254
+ {
255
+ "agent": {
256
+ "model": "claude-4-5-haiku",
257
+ "temperature": 0.5,
258
+ "maxTokens": 16000,
259
+ "systemPrompt": "system.md",
260
+ "tools": [
261
+ { "method": "create-todo", "description": "tools/createTodo.md" },
262
+ { "method": "list-todos", "description": "tools/listTodos.md" }
263
+ ],
264
+ "webInterfacePath": "/chat"
265
+ }
266
+ }
267
+ ```
268
+
269
+ **The token-limit field is renamed during compilation.** The spec frontmatter calls it
270
+ `maxResponseTokens`; the compiled `agent.json` calls it `maxTokens`. Same value, two names — carry it
271
+ across rather than copying the key.
272
+
273
+ | Field | Description |
274
+ |-------|-------------|
275
+ | `model` | MindStudio model ID. Comes from the spec frontmatter; look it up with `askMindStudioSdk` rather than guessing |
276
+ | `temperature` | Model temperature |
277
+ | `maxTokens` | Max response tokens (the spec's `maxResponseTokens`) |
278
+ | `systemPrompt` | Relative path to the compiled system prompt markdown file |
279
+ | `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) |
280
+ | `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. |
281
+
282
+ Declare it in `mindstudio.json`:
283
+
284
+ ```json
285
+ { "type": "agent", "path": "dist/interfaces/agent/agent.json" }
286
+ ```
287
+
288
+ ## Auth
289
+
290
+ Agent chat runs as the **authenticated user**, not as a system role — tool calls carry that user's
291
+ roles, so a method gated with `auth.requireRole` behaves exactly as it would if the user had called it
292
+ from the web frontend. That's what makes exposing real methods safe; it's also why role restrictions
293
+ belong in the tool descriptions, so the agent can decline gracefully instead of surfacing a rejection.
294
+
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: Inbound Email
3
+ what: The app has its own email address, and mail sent to it runs a method. Every address on the app's subdomain routes to one handler, so `support@`, `receipts@` and `anything@` all arrive without registering anything — branch on the recipient in code. Attachments arrive as CDN URLs the platform has already uploaded, threading headers come through intact so replies land in the same conversation, and a verified custom domain both receives and sends under the app's own brand. "Forward a receipt and the app files it" is a real feature that costs one method.
4
+ when: Before writing an email-handler method, adding an `email` interface, or promising anything about what happens when a user emails the app.
5
+ ---
6
+
7
+ # Inbound Email Interfaces
8
+
9
+ Inbound email triggers. Each app has **one** email-handler method; the platform routes all inbound
10
+ mail destined for the app — across any of its address tiers — to that method.
11
+
12
+ The addresses themselves are configured at the project level by the user through the Remy platform.
13
+ Your job is the `interface.json` and the method that handles the mail, not domain registration or MX
14
+ records.
15
+
16
+ ## Address tiers
17
+
18
+ Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart
19
+ registration); the legacy tier is specific-localpart and frozen for new apps.
20
+
21
+ | Tier | Address | How it's set up |
22
+ |---|---|---|
23
+ | 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. |
24
+ | 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. |
25
+ | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
26
+
27
+ Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on
28
+ it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`). This is what makes
29
+ per-purpose addresses free: you don't register `support@` anywhere, you just check for it.
30
+
31
+ A verified custom domain (and the app's `madewithremy.com` subdomain) also **sends** outbound mail, not
32
+ just receives — `sendEmail` picks the app's own-brand sender automatically, configured in the
33
+ dashboard's **Email** settings.
34
+
35
+ ## Config (`interface.json`)
36
+
37
+ The top-level key must match the interface type (`email`):
38
+
39
+ ```json
40
+ {
41
+ "email": {
42
+ "method": "handle-inbound-email",
43
+ "approvedSenders": ["billing@vendor.com", "*@trusted-partner.com"]
44
+ }
45
+ }
46
+ ```
47
+
48
+ `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com`
49
+ wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before
50
+ the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list
51
+ applies uniformly across all three address tiers.
52
+
53
+ Declare it in `mindstudio.json`:
54
+
55
+ ```json
56
+ { "type": "email", "path": "dist/interfaces/email/interface.json" }
57
+ ```
58
+
59
+ ## Input shape
60
+
61
+ ```ts
62
+ {
63
+ to: string; // full recipient address; localpart is arbitrary on catchall tiers
64
+ from: string; // bare sender address, extracted from "Name <a@b>" form
65
+ fromName: string | null; // sender display name, or null
66
+ subject: string; // 'No Subject' if missing
67
+ message: string; // plain-text body, falls back to HTML if text is missing; 'No Body' if neither was sent
68
+ html: string; // HTML body, or '' when text-only
69
+ attachments: string[]; // CDN URLs — already uploaded by the platform
70
+ messageId: string | null; // this email's Message-ID, angle-bracketed (<id@host>)
71
+ inReplyTo: string | null; // Message-ID this email is replying to, if any
72
+ references: string[]; // prior Message-IDs in the thread (angle-bracketed); [] if none
73
+ replyTo: string | null; // Reply-To address — reply here, not `from`, when set
74
+ cc: string[]; // Cc recipient addresses
75
+ date: string | null; // original send time, ISO-8601
76
+ }
77
+ ```
78
+
79
+ ## Replying in thread
80
+
81
+ Replies go out through the SDK's `sendEmail` action (its full parameter list is in the SDK actions
82
+ reference in your system prompt). Set `inReplyTo` to the incoming `messageId` and `references` to
83
+ `[...references, messageId]`. Send to `replyTo` when it's set, otherwise `from`.
84
+
85
+ ```typescript
86
+ await mindstudio.sendEmail({
87
+ to: input.replyTo ?? input.from,
88
+ subject: `Re: ${input.subject}`,
89
+ body: reply,
90
+ inReplyTo: input.messageId ?? undefined,
91
+ references: input.messageId
92
+ ? [...input.references, input.messageId]
93
+ : input.references,
94
+ cc: input.cc, // reply-all
95
+ });
96
+ ```
97
+
98
+ `sendEmail` returns `{ recipients, cc, bcc, from }` — who it sent to and the sender used. It does
99
+ **not** return the sent message's own `Message-ID`, so thread off *inbound* mail, never off messages
100
+ you sent.
101
+
102
+ ## Attachments and size limits
103
+
104
+ `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files.
105
+ Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or
106
+ downstream services.
107
+
108
+ Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by
109
+ the platform before the method runs.
110
+
111
+ ## Auth
112
+
113
+ Methods invoked through this interface run with `auth.roles: ['system']` — the platform is calling, not
114
+ a user session, so there's no user to impersonate. Use `auth.requireRole('system')` to gate methods that
115
+ should only be reachable via email. The auth reference in your system prompt covers the system role in
116
+ full.
@@ -1,16 +1,35 @@
1
1
  ---
2
2
  name: MCP Interfaces
3
3
  what: Ships the app as an MCP server, so external AI agents — Claude Desktop, Cursor, anyone's agent — can drive it as a tool surface. The platform hosts the server, handles auth, and derives every tool's input schema from the method contract, so there is no protocol code to write: the work is choosing which methods an outsider should see and describing them well enough for a stranger to use correctly. Cheap to add to an app that already has methods, and it puts the app inside the tools its users already work in.
4
- when: Before authoring `src/interfaces/mcp.md` or deciding which of the app's methods an external agent gets to see.
4
+ when: Before authoring `src/interfaces/mcp.md`, deciding which of the app's methods an external agent gets to see, or writing the MCP interface config.
5
5
  ---
6
6
 
7
- # Building MCP Interfaces
7
+ # MCP Interfaces
8
8
 
9
- Guidance for exposing an app as an MCP server — a tool / resource / prompt surface for *external* AI agents (Claude Desktop, Cursor, anyone's agent). The contract (spec format, compiled output, `interface.json`) is in the `<interfaces>` platform doc in your system prompt; this is how to author one well. Unlike the agent interface, there's no LLM, personality, or UI to design — the entire product is the descriptions and the shape of what you expose.
9
+ Exposing an app as an MCP server — a tool / resource / prompt surface for *external* AI agents (Claude
10
+ Desktop, Cursor, anyone's agent). Unlike the agent interface, which *is* an agent with its own LLM,
11
+ personality and chat UI, MCP has no model of its own: it's the app projected as a server for an outside
12
+ AI to drive.
13
+
14
+ It supports the full MCP surface:
15
+
16
+ - **Tools** — methods the agent can call (rich descriptions + machine-readable annotations).
17
+ - **Resources** — read-only app data the agent can pull into context, addressable by URI.
18
+ - **Prompts** — reusable, parameterized prompt templates the server offers.
19
+ - **Instructions** — server-level guidance shown to the calling agent (the toolset's "system prompt").
20
+
21
+ The platform hosts the server, handles auth, and derives every tool's input schema from the method
22
+ contract. So there's no protocol code to write, and the whole job is authorship plus a config file.
23
+ Authorship first, since that's what decides whether the toolset actually works.
10
24
 
11
25
  ## The descriptions are the product
12
26
 
13
- The calling agent is a stranger with no knowledge of your app. It decides what to invoke entirely from the names, descriptions, and annotations you ship. Follow the same principles as the agent interface's tool descriptions (load the `agentInterfaces` skill for those — when to use and when not, parameter guidance beyond the schema, what the tool returns) — but write them **self-contained**. An in-app agent tool can lean on the app's framing; an MCP tool can't, because the caller has no context. Spell out what an outsider wouldn't know.
27
+ The calling agent is a stranger with no knowledge of your app. It decides what to invoke entirely from
28
+ the names, descriptions, and annotations you ship. Follow the same principles as the agent interface's
29
+ tool descriptions (load the `agentInterfaces` skill for those — when to use and when not, parameter
30
+ guidance beyond the schema, what the tool returns) — but write them **self-contained**. An in-app agent
31
+ tool can lean on the app's framing; an MCP tool can't, because the caller has no context. Spell out what
32
+ an outsider wouldn't know.
14
33
 
15
34
  What that looks like in practice — the same method, described twice:
16
35
 
@@ -28,46 +47,69 @@ full updated vendor. Editors and admins only; other roles are rejected. For a
28
47
  vendor that doesn't exist yet, use `createVendor`.
29
48
  ```
30
49
 
31
- The weak one is what a schema already tells the caller. The strong one carries the three things a schema can't: the prerequisite, the partial-update semantics, and the alternative when this isn't the right tool.
50
+ The weak one is what a schema already tells the caller. The strong one carries the three things a schema
51
+ can't: the prerequisite, the partial-update semantics, and the alternative when this isn't the right
52
+ tool.
32
53
 
33
54
  ## Curate — not every method is a tool
34
55
 
35
- Expose what an outside agent would actually use. Skip internal helpers, admin-only methods, and batch operations. A focused set of well-described tools beats a large set of thin ones. Note role restrictions in the description — gated tools are listed but reject unauthorized calls at runtime, so set expectations rather than surfacing a raw error.
56
+ Expose what an outside agent would actually use. Skip internal helpers, admin-only methods, and batch
57
+ operations. A focused set of well-described tools beats a large set of thin ones. Note role restrictions
58
+ in the description — gated tools are listed but reject unauthorized calls at runtime, so set
59
+ expectations rather than surfacing a raw error.
36
60
 
37
61
  ## Annotations
38
62
 
39
- Annotations are machine-readable hints clients use to decide whether to auto-call a tool or ask the user first. Set them honestly:
63
+ Annotations are machine-readable hints clients use to decide whether to auto-call a tool or ask the user
64
+ first. Set them honestly:
40
65
 
41
- - `readOnly` — the tool only reads, never mutates. The highest-value hint: clients auto-call reads without prompting, so set it on every pure read.
66
+ - `readOnly` — the tool only reads, never mutates. The highest-value hint: clients auto-call reads
67
+ without prompting, so set it on every pure read.
42
68
  - `destructive` — the tool can delete or overwrite. Clients gate these behind confirmation.
43
69
  - `idempotent` — calling twice with the same arguments has the same effect as calling once.
44
- - `openWorld` — the tool reaches outside the app (external web/services) rather than operating only on app data.
70
+ - `openWorld` — the tool reaches outside the app (external web/services) rather than operating only on
71
+ app data.
45
72
 
46
- The judgement is per tool, and getting `readOnly` right is what makes a toolset feel responsive rather than nagging:
73
+ The judgement is per tool, and getting `readOnly` right is what makes a toolset feel responsive rather
74
+ than nagging. Abbreviated to just the annotations (a real entry also carries `name`, `title` and
75
+ `description` — the Config section has the full shape):
47
76
 
48
77
  ```jsonc
49
- "getVendor": { "readOnly": true, "idempotent": true }
50
- "searchVendors": { "readOnly": true, "idempotent": true }
51
- "updateVendor": { "idempotent": true } // repeatable, but it writes
52
- "deleteVendor": { "destructive": true, "idempotent": true }
53
- "enrichVendorFromWeb": { "openWorld": true } // calls out to the internet
78
+ "tools": [
79
+ { "method": "get-vendor", "annotations": { "readOnly": true, "idempotent": true } },
80
+ { "method": "search-vendors", "annotations": { "readOnly": true, "idempotent": true } },
81
+ { "method": "update-vendor", "annotations": { "idempotent": true } }, // repeatable, but it writes
82
+ { "method": "delete-vendor", "annotations": { "destructive": true, "idempotent": true } },
83
+ { "method": "enrich-vendor", "annotations": { "openWorld": true } } // calls out to the internet
84
+ ]
54
85
  ```
55
86
 
56
- Set them honestly rather than defensively. Marking a read `destructive` to be safe means the caller's user gets a confirmation prompt for looking something up, and they will stop reading the prompts.
87
+ Set them honestly rather than defensively. Marking a read `destructive` to be safe means the caller's
88
+ user gets a confirmation prompt for looking something up, and they will stop reading the prompts.
57
89
 
58
90
  ## Tools vs. resources
59
91
 
60
- A **tool** is an action the agent *invokes*; a **resource** is data the agent *reads into context*. A read-only method can be either — expose it as a tool if the agent will call it as a step, as a resource if it's reference data the agent should pull in, and as both when both fit.
92
+ A **tool** is an action the agent *invokes*; a **resource** is data the agent *reads into context*. A
93
+ read-only method can be either — expose it as a tool if the agent will call it as a step, as a resource
94
+ if it's reference data the agent should pull in, and as both when both fit.
61
95
 
62
- Resources are method-backed: a read invokes the method. Use a static `uri` for a fixed collection (`app://vendors`) and a `uriTemplate` when the read takes parameters (`app://vendors/{id}`, where `{id}` maps to the method's input). Keep URIs stable and human-legible.
96
+ Resources are method-backed: a read invokes the method. Use a static `uri` for a fixed collection
97
+ (`app://vendors`) and a `uriTemplate` when the read takes parameters (`app://vendors/{id}`, where `{id}`
98
+ maps to the method's input). Keep URIs stable and human-legible.
63
99
 
64
100
  ## Prompts
65
101
 
66
- Prompts are reusable, parameterized templates the server offers to clients — e.g. a "draft a vendor email" starter. Author the template body with `{{arg}}` placeholders and declare its arguments. Offer a prompt when there's a recurring task worth packaging; skip it if a tool already covers the need.
102
+ Prompts are reusable, parameterized templates the server offers to clients — e.g. a "draft a vendor
103
+ email" starter. Author the template body with `{{arg}}` placeholders and declare its arguments. Offer a
104
+ prompt when there's a recurring task worth packaging; skip it if a tool already covers the need.
67
105
 
68
106
  ## Server instructions
69
107
 
70
- The spec's intro prose becomes the server `instructions` — toolset-level guidance returned to the calling agent at connect time (its "system prompt"). Put *cross-cutting* guidance here: how the tools fit together, ordering or prerequisites ("read a vendor before updating it"), and norms that apply across the whole toolset. Keep per-tool specifics in the tool descriptions; instructions are for the toolset as a whole.
108
+ The spec's intro prose becomes the server `instructions` — toolset-level guidance returned to the
109
+ calling agent at connect time (its "system prompt"). Put *cross-cutting* guidance here: how the tools
110
+ fit together, ordering or prerequisites ("read a vendor before updating it"), and norms that apply
111
+ across the whole toolset. Keep per-tool specifics in the tool descriptions; instructions are for the
112
+ toolset as a whole.
71
113
 
72
114
  ```markdown
73
115
  This server exposes a procurement app. Vendors are the central record and
@@ -77,4 +119,162 @@ search tool before calling anything that takes one. Search results are capped
77
119
  at 50; page with the returned cursor rather than broadening the query.
78
120
  ```
79
121
 
80
- That's four sentences doing what no individual tool description could: it explains the shape of the domain, so the calling agent's first move is a reasonable one.
122
+ That's four sentences doing what no individual tool description could: it explains the shape of the
123
+ domain, so the calling agent's first move is a reasonable one.
124
+
125
+ ---
126
+
127
+ # The wiring
128
+
129
+ ## Spec: `src/interfaces/mcp.md`
130
+
131
+ Frontmatter declares the server. In the body, the intro prose becomes the server `instructions`, and
132
+ `## Tools`, `## Resources` and `## Prompts` headings declare the rest.
133
+
134
+ ```yaml
135
+ ---
136
+ name: Vendor Management
137
+ description: Tools and data for managing vendors and purchase orders.
138
+ type: interface/mcp
139
+ ---
140
+ ```
141
+
142
+ ```markdown
143
+ This server manages vendors and purchase orders. Read a vendor before updating it; submitted
144
+ requests go through approval before they become active.
145
+
146
+ ## Tools
147
+
148
+ ### Submit a vendor request
149
+ method: submit-vendor-request
150
+ ~~~
151
+ Submit a new vendor for approval. Use when the caller wants to add a vendor.
152
+ Do NOT use to modify an existing vendor — that's update-vendor.
153
+ - name: the vendor's legal name
154
+ - contactEmail: billing contact; required for approval routing
155
+ Returns the created vendor's id and its initial "pending" status.
156
+ ~~~
157
+
158
+ ### List vendors
159
+ method: list-vendors
160
+ annotations: readOnly
161
+ ~~~
162
+ List all vendors, newest first. Read-only.
163
+ ~~~
164
+
165
+ ## Resources
166
+
167
+ - list-vendors → app://vendors — "Vendors" — all vendors (application/json)
168
+ - get-vendor → app://vendors/{id} — "Vendor" — a single vendor by id (application/json)
169
+
170
+ ## Prompts
171
+
172
+ ### draft_vendor_email
173
+ description: Draft an outreach email to a vendor.
174
+ arguments: vendorId (required) — the vendor to contact
175
+ ~~~
176
+ Write a warm outreach email to vendor {{vendorId}} introducing our procurement process.
177
+ ~~~
178
+ ```
179
+
180
+ Don't hand-author input schemas — the platform derives them.
181
+
182
+ ## Compiled Output: `dist/interfaces/mcp/`
183
+
184
+ ```
185
+ dist/interfaces/mcp/
186
+ ├── interface.json ← config the platform reads
187
+ ├── instructions.md ← server-level guidance (returned in `initialize`)
188
+ ├── tools/
189
+ │ ├── submitVendorRequest.md ← rich description, one per tool
190
+ │ └── listVendors.md
191
+ └── prompts/
192
+ └── draftVendorEmail.md ← prompt template body, one per prompt
193
+ ```
194
+
195
+ Resources carry inline metadata only — no per-resource file.
196
+
197
+ ## Config (`interface.json`)
198
+
199
+ The top-level key must match the interface type (`mcp`):
200
+
201
+ ```json
202
+ {
203
+ "mcp": {
204
+ "name": "Vendor Management",
205
+ "description": "Tools and data for managing vendors and purchase orders.",
206
+ "instructions": "instructions.md",
207
+ "tools": [
208
+ {
209
+ "method": "submit-vendor-request",
210
+ "name": "submit_vendor_request",
211
+ "title": "Submit Vendor Request",
212
+ "description": "tools/submitVendorRequest.md",
213
+ "annotations": { "readOnly": false, "destructive": false, "idempotent": false, "openWorld": false }
214
+ },
215
+ {
216
+ "method": "list-vendors",
217
+ "title": "List Vendors",
218
+ "description": "tools/listVendors.md",
219
+ "annotations": { "readOnly": true }
220
+ }
221
+ ],
222
+ "resources": [
223
+ { "method": "list-vendors", "uri": "app://vendors", "name": "Vendors", "description": "All vendors.", "mimeType": "application/json" },
224
+ { "method": "get-vendor", "uriTemplate": "app://vendors/{id}", "name": "Vendor", "description": "A single vendor by id.", "mimeType": "application/json" }
225
+ ],
226
+ "prompts": [
227
+ {
228
+ "name": "draft_vendor_email",
229
+ "title": "Draft vendor email",
230
+ "description": "Draft an outreach email to a vendor.",
231
+ "arguments": [ { "name": "vendorId", "description": "The vendor to contact", "required": true } ],
232
+ "template": "prompts/draftVendorEmail.md"
233
+ }
234
+ ]
235
+ }
236
+ }
237
+ ```
238
+
239
+ | Field | Description |
240
+ |-------|-------------|
241
+ | `name`, `description` | Server display name + registry metadata (not shown to the calling agent) |
242
+ | `instructions` | Relative path to the server-level guidance returned in `initialize` |
243
+ | `tools[].method` | Method `id` from the manifest (kebab-case) |
244
+ | `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 |
245
+ | `tools[].title` | Optional human-friendly display name |
246
+ | `tools[].description` | Relative path to the tool's markdown description |
247
+ | `tools[].annotations` | The client hints from the Annotations section: `readOnly`, `destructive`, `idempotent`, `openWorld` — they map to MCP's `readOnlyHint` etc. |
248
+ | `resources[].method` | The read method invoked when the resource is read |
249
+ | `resources[].uri` / `uriTemplate` | A static URI, or a template whose `{param}` maps to the method's input |
250
+ | `resources[].name`, `description`, `mimeType` | Resource metadata |
251
+ | `prompts[].name`, `title`, `description` | Prompt identity + metadata |
252
+ | `prompts[].arguments` | `[{ name, description?, required? }]` |
253
+ | `prompts[].template` | Relative path to the template body (`{{arg}}` placeholders) |
254
+
255
+ There is no `inputSchema` field — the platform derives each tool's schema from the method's input
256
+ contract.
257
+
258
+ Declare it in `mindstudio.json`:
259
+
260
+ ```json
261
+ { "type": "mcp", "path": "dist/interfaces/mcp/interface.json" }
262
+ ```
263
+
264
+ ## Platform Behavior
265
+
266
+ - The platform hosts the MCP server and exposes it to external clients. Clients connect at
267
+ `POST https://{app-host}/_/mcp`, where `{app-host}` is any host the app is served on: its
268
+ `custom_subdomain` host (e.g. `myapp.madewithremy.com`), a custom domain if configured, or the UUID
269
+ host (`<appId>.madewithremy.com` / `.msagent.ai`).
270
+ - **Auth is optional.** A `Bearer` key resolves to a user with full RBAC, so the method's own
271
+ `auth.requireRole`/`requireUser` checks apply as they would for that user. With no key, calls run
272
+ anonymously — no user, no roles. The method is the boundary: gate sensitive tools, and understand that
273
+ a public (keyless) server effectively exposes only the un-gated ones.
274
+ - Input schemas are derived automatically from each method's input contract.
275
+ - `tools/list` is static; access is enforced per-method at call time (a gated tool is listed but rejects
276
+ an unauthorized call).
277
+ - A resource read invokes the backing method (template `{param}`s come from the URI) and returns its
278
+ output as the resource contents.
279
+ - `prompts/get` fills the template with the provided arguments.
280
+ - `instructions` is returned in the `initialize` response.
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: REST API
3
+ what: A designed, documented REST surface over the app's methods — named routes, path and query params, resource groupings, and a generated OpenAPI spec. This is distinct from the method endpoints every app already has: those exist automatically and need nothing from you. This is for when the API itself is the product, or when something outside the app has to integrate against stable URLs rather than internal method names.
4
+ when: Before authoring `src/interfaces/api.md` or adding an `api` interface with designed routes. Also load it when a method needs the raw HTTP request (headers, unparsed body).
5
+ ---
6
+
7
+ # REST API Interfaces
8
+
9
+ REST endpoints for external consumers — other services, mobile apps, integrations. This is separate
10
+ from the web frontend's internal RPC (`@mindstudio-ai/interface` calls `/_/methods` directly and does
11
+ not use the API interface). The API interface lives at `/_/api/` and exposes only the methods you
12
+ choose to route.
13
+
14
+ Use it for sync endpoints for other services, a public REST API, batch tools — anything where
15
+ something outside the app's own frontend needs to call a method over HTTP.
16
+
17
+ **For provider webhooks (Stripe, GitHub, Shopify) there are two native paths**, and they are easy to
18
+ confuse. This interface handles them with bearer auth and `input._request.rawBody`. The Webhook
19
+ interface handles them with secret-in-URL routing and a top-level `input.rawBody` — usually the better
20
+ fit for provider callbacks, since providers can't send a bearer token. Load the `webhooks` skill before
21
+ choosing.
22
+
23
+ ## Spec: `src/interfaces/api.md`
24
+
25
+ The human-readable spec. Frontmatter declares the API name and description; the body maps methods to
26
+ REST routes using MSFM.
27
+
28
+ ```yaml
29
+ ---
30
+ name: Vendor Management API
31
+ description: API for managing vendors and purchase orders.
32
+ type: interface/api
33
+ ---
34
+ ```
35
+
36
+ Routes are declared as `VERB /path → methodExportName` under resource headings, with annotations for
37
+ params and descriptions:
38
+
39
+ ```markdown
40
+ ## Vendors
41
+
42
+ ### List vendors
43
+ GET /vendors → listVendors
44
+ ~~~
45
+ Returns all vendors, optionally filtered by status.
46
+ query: status (string, optional) — filter by vendor status
47
+ ~~~
48
+
49
+ ### Create vendor
50
+ POST /vendors → submitVendorRequest
51
+ ~~~
52
+ Submit a new vendor for approval.
53
+ body: name (string, required) — vendor name
54
+ contactEmail (string, required) — billing contact
55
+ ~~~
56
+
57
+ ### Delete vendor
58
+ DELETE /vendors/:vendorId → deleteVendor
59
+ ~~~
60
+ path: vendorId (string, required) — the vendor's unique identifier
61
+ ~~~
62
+ ```
63
+
64
+ ## Compiled Output: `dist/interfaces/api/api.json`
65
+
66
+ ```json
67
+ {
68
+ "api": {
69
+ "name": "Vendor Management API",
70
+ "description": "API for managing vendors and purchase orders.",
71
+ "routes": [
72
+ {
73
+ "method": "GET",
74
+ "path": "/vendors",
75
+ "handler": "list-vendors",
76
+ "summary": "List vendors",
77
+ "description": "Returns all vendors, optionally filtered by status.",
78
+ "tag": "Vendors",
79
+ "params": {
80
+ "query": {
81
+ "status": { "type": "string", "required": false, "description": "Filter by vendor status" }
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "method": "POST",
87
+ "path": "/vendors",
88
+ "handler": "submit-vendor-request",
89
+ "summary": "Create vendor",
90
+ "description": "Submit a new vendor for approval.",
91
+ "tag": "Vendors",
92
+ "params": {
93
+ "body": {
94
+ "name": { "type": "string", "required": true, "description": "Vendor name" },
95
+ "contactEmail": { "type": "string", "required": true, "description": "Billing contact" }
96
+ }
97
+ }
98
+ },
99
+ {
100
+ "method": "DELETE",
101
+ "path": "/vendors/:vendorId",
102
+ "handler": "delete-vendor",
103
+ "summary": "Delete vendor",
104
+ "description": "Permanently remove a vendor.",
105
+ "tag": "Vendors",
106
+ "params": {
107
+ "path": {
108
+ "vendorId": { "type": "string", "required": true, "description": "The vendor's unique identifier" }
109
+ }
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ }
115
+ ```
116
+
117
+ | Field | Description |
118
+ |-------|-------------|
119
+ | `name` | API display name (used in generated OpenAPI spec) |
120
+ | `description` | API description |
121
+ | `routes[].method` | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
122
+ | `routes[].path` | URL path with `:param` placeholders for path params |
123
+ | `routes[].handler` | Method `id` from the manifest (kebab-case) |
124
+ | `routes[].summary` | Short description for the endpoint |
125
+ | `routes[].description` | Longer description |
126
+ | `routes[].tag` | Resource grouping (becomes a tag in OpenAPI) |
127
+ | `routes[].params` | Parameter declarations: `path`, `query`, and/or `body` objects |
128
+
129
+ Declare it in `mindstudio.json`:
130
+
131
+ ```json
132
+ { "type": "api", "path": "dist/interfaces/api/api.json" }
133
+ ```
134
+
135
+ ## Platform Behavior
136
+
137
+ Routes are mounted at `/_/api{path}` (e.g. `DELETE /_/api/vendors/abc123`).
138
+
139
+ - **Path params** are extracted and merged into the method's input: `/:vendorId` → `{ vendorId: "abc123" }`
140
+ - **Query params** are merged into input for GET requests: `?status=approved` → `{ status: "approved" }`
141
+ - **Request body** for POST/PUT/PATCH is the input directly (no `{ input: {...} }` wrapper)
142
+ - **Response** is the method output directly (no `{ output: {...} }` wrapper)
143
+ - **Auth** via `Authorization: Bearer sk_...` — an API key resolves to a user with full RBAC, so the
144
+ method's own `auth.requireRole`/`requireUser` checks apply exactly as they would for that user
145
+ - **Streaming**: `Accept: text/event-stream` header returns SSE chunks
146
+ - **Raw request context**: Every API method receives `input._request` with `{ method, headers, rawBody }`.
147
+ `rawBody` is the original unparsed body as a UTF-8 string — needed for signature verification, since
148
+ providers HMAC the raw payload and a re-serialized body won't match. For most methods you don't need
149
+ `_request` at all.