@mindstudio-ai/remy 0.1.276 → 0.1.277

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.
@@ -6,33 +6,20 @@ when: Only for unstructured documents queried by meaning — "find the clause ab
6
6
 
7
7
  # Data Sources (Search Over Documents)
8
8
 
9
- Per-app searchable document corpora: upload documents, ask in plain language, get back the passages
10
- that answer it with a citation to the source.
9
+ Per-app searchable document corpora: upload documents, ask in plain language, get back the passages that answer it with a citation to the source.
11
10
 
12
- **Most apps should not use one — check this before reaching for it.** Structured data (rows with
13
- fields you filter on) belongs in `db`; a `WHERE` clause is faster, cheaper and exact. A data source
14
- earns its cost only for **unstructured documents queried by meaning**. "Find the clause about early
15
- termination" is a data source. "Find contracts signed after March" is a `db` query. If the question
16
- can be expressed as a filter, it is not a search problem.
11
+ **Most apps should not use one — check this before reaching for it.** Structured data (rows with fields you filter on) belongs in `db`; a `WHERE` clause is faster, cheaper and exact. A data source earns its cost only for **unstructured documents queried by meaning**. "Find the clause about early termination" is a data source. "Find contracts signed after March" is a `db` query. If the question can be expressed as a filter, it is not a search problem.
17
12
 
18
- Don't use one when: the data is structured (`db`), you only need to store files (`files` — nobody is
19
- searching the contents), the requirement is exact lookup by identifier, or the corpus is a handful of
20
- short docs that fit in a prompt.
13
+ Don't use one when: the data is structured (`db`), you only need to store files (`files` — nobody is searching the contents), the requirement is exact lookup by identifier, or the corpus is a handful of short docs that fit in a prompt.
21
14
 
22
15
  ## Behaviour (read before the API)
23
16
 
24
- - **One corpus shared across dev and prod** — like a file store, not a table. No dev copy, no
25
- per-release isolation. A document added while building is already live.
17
+ - **One corpus shared across dev and prod** — like a file store, not a table. No dev copy, no per-release isolation. A document added while building is already live.
26
18
  - **Scenarios never reset a data source.** Don't write `clear()`-style reset helpers.
27
19
  - **Re-adding the same bytes is free** — content-addressed, so ingest scripts are safe to re-run.
28
20
  - **Ingest is async.** `add()` returns once queued; poll `documents()`, or use `--wait` from the CLI.
29
21
  - **Reprocessing costs real money**, so changing how a corpus is built is always explicit.
30
- - **Limits apply**: 25 data sources per app, 5,000 documents per source, 10,000 chunks per
31
- document, 300 searches/minute. Well clear of normal use — but **source names must be fixed, not
32
- computed per user or per request**, since referencing one creates it. Partition inside a source
33
- with document metadata instead: tag at add time
34
- (`add(bytes, { filename, metadata: { userId } })`), narrow at search time
35
- (`search(q, { filter: { metadata: { userId } } })`).
22
+ - **Limits apply**: 25 data sources per app, 5,000 documents per source, 10,000 chunks per document, 300 searches/minute. Well clear of normal use — but **source names must be fixed, not computed per user or per request**, since referencing one creates it. Partition inside a source with document metadata instead: tag at add time (`add(bytes, { filename, metadata: { userId } })`), narrow at search time (`search(q, { filter: { metadata: { userId } } })`).
36
23
 
37
24
  ## Defining and searching
38
25
 
@@ -44,43 +31,21 @@ const { results } = await Policies.search('what are the payment terms?', { topK:
44
31
  const context = results.map((r) => r.text).join('\n\n');
45
32
  ```
46
33
 
47
- Hits are `{ score, text, citation }` with
48
- `citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url }`, plus
49
- `retrievalRank`/`retrievalScore` — the position before reranking, so you can show what reranking did.
34
+ Hits are `{ score, text, citation }` with `citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url }`, plus `retrievalRank`/`retrievalScore` — the position before reranking, so you can show what reranking did.
50
35
 
51
- **Always render the citation.** `citation.url` is a stable on-domain link — put it in an `<a href>`
52
- beside the answer. Retrieval is approximate; a user who can click through can judge for themselves.
53
- An answer with no citation is an assertion.
36
+ **Always render the citation.** `citation.url` is a stable on-domain link — put it in an `<a href>` beside the answer. Retrieval is approximate; a user who can click through can judge for themselves. An answer with no citation is an assertion.
54
37
 
55
- Created on first use, so searching a source the build hasn't populated returns no results rather than
56
- throwing. `search` options: `topK` (default 5, max 50), `scoreThreshold`, `filter`, `mode`,
57
- `maxPerDocument`, `highlight`, `rerank`, `hybrid`.
38
+ Created on first use, so searching a source the build hasn't populated returns no results rather than throwing. `search` options: `topK` (default 5, max 50), `scoreThreshold`, `filter`, `mode`, `maxPerDocument`, `highlight`, `rerank`, `hybrid`.
58
39
 
59
- **Filtering** narrows a search before ranking, and every condition only narrows:
60
- `filter: { metadata: { department: 'legal', year: [2025, 2026] }, filename, documentIds,
61
- pages: { min?, max? }, contains: 'all these words', phrase: 'exact adjacent sequence' }`.
62
- Metadata is tagged at add time (scalars only, ≤16 keys); re-adding the same bytes with different
63
- metadata updates the tags in place, free. Filters are the right tool for scoping retrieval
64
- (per-user, per-category); they are NOT a substitute for a `db` query over structured data.
40
+ **Filtering** narrows a search before ranking, and every condition only narrows: `filter: { metadata: { department: 'legal', year: [2025, 2026] }, filename, documentIds, pages: { min?, max? }, contains: 'all these words', phrase: 'exact adjacent sequence' }`. Metadata is tagged at add time (scalars only, ≤16 keys); re-adding the same bytes with different metadata updates the tags in place, free. Filters are the right tool for scoping retrieval (per-user, per-category); they are NOT a substitute for a `db` query over structured data.
65
41
 
66
- **Modes**: `mode: 'hybrid'` (default) fuses semantic and keyword retrieval; `'semantic'` is the
67
- embedding alone; `'lexical'` is keyword-only with **no query embedding** — cheapest and fastest,
68
- right when the query is an identifier (an error code, a SKU, a name) rather than a meaning.
69
- `maxPerDocument: 2` stops one document monopolizing the results when the answer should draw on
70
- several. `highlight: true` adds `matches` (`{start, end}` offsets into `text`) for rendering
71
- highlighted excerpts.
42
+ **Modes**: `mode: 'hybrid'` (default) fuses semantic and keyword retrieval; `'semantic'` is the embedding alone; `'lexical'` is keyword-only with **no query embedding** — cheapest and fastest, right when the query is an identifier (an error code, a SKU, a name) rather than a meaning. `maxPerDocument: 2` stops one document monopolizing the results when the answer should draw on several. `highlight: true` adds `matches` (`{start, end}` offsets into `text`) for rendering highlighted excerpts.
72
43
 
73
- Search is deterministic for a fixed corpus and configuration, so eval sets and regression checks are
74
- meaningful — key them on `(documentId, chunkIndex)` rather than on chunk text.
44
+ Search is deterministic for a fixed corpus and configuration, so eval sets and regression checks are meaningful — key them on `(documentId, chunkIndex)` rather than on chunk text.
75
45
 
76
- **Debugging retrieval.** Two opt-in options, neither of which changes the results or their order:
77
- `explain: true` adds `explain.{dense, lexical, matchedVia}` (which half of hybrid found each hit;
78
- costs two extra round trips), and `expand: 1` adds `neighbors.{before, after}` for surrounding
79
- context. When a document never comes back at all, `Policies.stats()` reports the config actually in
80
- effect and `Policies.chunks(documentId)` shows exactly how it was split.
46
+ **Debugging retrieval.** Two opt-in options, neither of which changes the results or their order: `explain: true` adds `explain.{dense, lexical, matchedVia}` (which half of hybrid found each hit; costs two extra round trips), and `expand: 1` adds `neighbors.{before, after}` for surrounding context. When a document never comes back at all, `Policies.stats()` reports the config actually in effect and `Policies.chunks(documentId)` shows exactly how it was split.
81
47
 
82
- **Configuration is not declared in code** — chunking and embedding settings live on the corpus and are
83
- set with the CLI, so code and reality can't drift.
48
+ **Configuration is not declared in code** — chunking and embedding settings live on the corpus and are set with the CLI, so code and reality can't drift.
84
49
 
85
50
  ## Loading documents — normally at build time, from the CLI
86
51
 
@@ -92,12 +57,9 @@ mindstudio-prod datasources search --source policies --filter department=legal -
92
57
  mindstudio-prod datasources delete --source policies # whole source; --source is required, never defaulted
93
58
  ```
94
59
 
95
- `--wait` blocks until processing finishes and exits non-zero on failure. Also `datasources list`,
96
- `status` (per-document state + ingest errors), `rm --document <id>`. `--help` for flags.
60
+ `--wait` blocks until processing finishes and exits non-zero on failure. Also `datasources list`, `status` (per-document state + ingest errors), `rm --document <id>`. `--help` for flags.
97
61
 
98
- **Seeding a test corpus:** scenarios don't touch data sources, so load fixtures with the same command
99
- in a setup script — `datasources add --source <slug> --wait fixtures/*.pdf`. Re-running is free, so
100
- it needs no guard.
62
+ **Seeding a test corpus:** scenarios don't touch data sources, so load fixtures with the same command in a setup script — `datasources add --source <slug> --wait fixtures/*.pdf`. Re-running is free, so it needs no guard.
101
63
 
102
64
  Use the SDK's `add()` only when *users* upload documents that must become searchable:
103
65
 
@@ -115,9 +77,7 @@ Formats: pdf, docx, pptx, xlsx, odt, rtf, epub, images, txt, md, json, csv, tsv,
115
77
 
116
78
  ## Answering from results
117
79
 
118
- Retrieve → join passages as context → have a model answer *from that context* → render citations.
119
- Never paste raw chunks at the user; they're fragments. For agentic flows, give the model `search` as a
120
- tool so it can query repeatedly and refine, rather than retrieving once up front.
80
+ Retrieve → join passages as context → have a model answer *from that context* → render citations. Never paste raw chunks at the user; they're fragments. For agentic flows, give the model `search` as a tool so it can query repeatedly and refine, rather than retrieving once up front.
121
81
 
122
82
  ## Tuning — two kinds of setting
123
83
 
@@ -126,21 +86,16 @@ tool so it can query repeatedly and refine, rather than retrieving once up front
126
86
  | **Free** (ranking) | `--rerank`, `--rerank-model`, `--hybrid`, `--top-k` | none, next search |
127
87
  | **Rebuild** (how docs become vectors) | `--max-chars`, `--min-chars`, `--drop-blocks`, `--contextual`, `--describe-images`, `--embedding-model`, `--extraction-model` | every document reprocessed |
128
88
 
129
- Images inside documents are described by a vision model and the description substituted into the
130
- searchable text (`--describe-images`, on by default) — without it a chart contributes nothing to
131
- search at all. Documents with no images cost nothing.
89
+ Images inside documents are described by a vision model and the description substituted into the searchable text (`--describe-images`, on by default) — without it a chart contributes nothing to search at all. Documents with no images cost nothing.
132
90
 
133
- `rerank` and `hybrid` default on and are usually right — reranking is the biggest quality lever, and
134
- hybrid is what finds part numbers, error codes and proper nouns a semantic model never learned. Both
135
- are also per-query (`search(q, { rerank: false })`) for a latency-sensitive path.
91
+ `rerank` and `hybrid` default on and are usually right — reranking is the biggest quality lever, and hybrid is what finds part numbers, error codes and proper nouns a semantic model never learned. Both are also per-query (`search(q, { rerank: false })`) for a latency-sensitive path.
136
92
 
137
93
  ```bash
138
94
  mindstudio-prod datasources config --source policies # show
139
95
  mindstudio-prod datasources config --source policies --top-k 8 # free, immediate
140
96
  ```
141
97
 
142
- **A rebuild-class change on a populated corpus is rejected** — you're told what it would invalidate
143
- and what it costs. To make it, build a new version alongside the live one:
98
+ **A rebuild-class change on a populated corpus is rejected** — you're told what it would invalidate and what it costs. To make it, build a new version alongside the live one:
144
99
 
145
100
  ```bash
146
101
  mindstudio-prod datasources revectorize --source policies --max-chars 900 --wait
@@ -148,7 +103,6 @@ mindstudio-prod datasources search --source policies --candidate "payment terms"
148
103
  mindstudio-prod datasources promote --source policies # go live
149
104
  ```
150
105
 
151
- Search serves the current version throughout, so nothing degrades while the new one builds.
152
- `datasources drop` discards an unwanted candidate.
106
+ Search serves the current version throughout, so nothing degrades while the new one builds. `datasources drop` discards an unwanted candidate.
153
107
 
154
108
  For anything deeper on the SDK, ask `askMindStudioSdk` rather than guessing at an API.
@@ -6,37 +6,19 @@ when: Before defining a file store or writing any upload, download, share-link,
6
6
 
7
7
  # Files & Storage
8
8
 
9
- Per-app blob storage: user uploads, generated documents, images, marketing assets. **Think of a store
10
- as a CDN-backed bucket the app talks to — not app-defined state like the database.** You declare the
11
- store; what lands in it is arbitrary durable blobs the app doesn't model — no schema, nothing to
12
- migrate, no dev/prod sync. **Private by default**, served on the app's own domain. (The API is
13
- *shaped* like `db` — `defineStore` at module scope, import the handle, like `defineTable` — but the
14
- mental model is a bucket, not rows.)
9
+ Per-app blob storage: user uploads, generated documents, images, marketing assets. **Think of a store as a CDN-backed bucket the app talks to — not app-defined state like the database.** You declare the store; what lands in it is arbitrary durable blobs the app doesn't model — no schema, nothing to migrate, no dev/prod sync. **Private by default**, served on the app's own domain. (The API is *shaped* like `db` — `defineStore` at module scope, import the handle, like `defineTable` — but the mental model is a bucket, not rows.)
15
10
 
16
11
  ## How a store behaves (read before the API)
17
12
 
18
- - **One store, shared across dev and prod — on purpose.** There's no dev copy: a file you upload in
19
- the dev editor (a marketing image, a corpus to vectorize) is *already there in prod* at the same
20
- stable URL. That continuity is a feature — don't fork buckets per environment.
21
- - **Creates are safe by default**, because keys default to unique — `put()` mints a UUID (or a
22
- content-addressed hash) when you don't pass one, so a dev write and a prod write land at different
23
- keys and coexist. A collision only happens when you *choose* a fixed key.
24
- - **Care goes on the destructive / fixed-key operations**, not on writing in general: `delete(key)`
25
- and overwriting a **stable key** (e.g. `config/latest.json`) reach the one live store, so a dev run
26
- can clobber what prod serves. A `put()` with a default key can't. (There's intentionally no bulk
27
- "clear the store".)
28
- - **Scenarios don't touch files — and that's correct.** A scenario truncates DB tables to seed test
29
- *rows*; files are durable and left alone. A store isn't a "clean slate" you re-seed each run — upload
30
- a test file once in dev and it stays. Accumulation is normal for an asset store; don't write
31
- `clear()`-style reset helpers.
32
- - **Need dev and prod to *not* share** something (mutable fixed-key state, or sensitive uploads a
33
- developer shouldn't see)? There's no per-store isolation switch — scope the key yourself (e.g.
34
- `config/${env}/…`). Rare; the shared default is right almost always.
13
+ - **One store, shared across dev and prod — on purpose.** There's no dev copy: a file you upload in the dev editor (a marketing image, a corpus to vectorize) is *already there in prod* at the same stable URL. That continuity is a feature — don't fork buckets per environment.
14
+ - **Creates are safe by default**, because keys default to unique — `put()` mints a UUID (or a content-addressed hash) when you don't pass one, so a dev write and a prod write land at different keys and coexist. A collision only happens when you *choose* a fixed key.
15
+ - **Care goes on the destructive / fixed-key operations**, not on writing in general: `delete(key)` and overwriting a **stable key** (e.g. `config/latest.json`) reach the one live store, so a dev run can clobber what prod serves. A `put()` with a default key can't. (There's intentionally no bulk "clear the store".)
16
+ - **Scenarios don't touch files — and that's correct.** A scenario truncates DB tables to seed test *rows*; files are durable and left alone. A store isn't a "clean slate" you re-seed each run — upload a test file once in dev and it stays. Accumulation is normal for an asset store; don't write `clear()`-style reset helpers.
17
+ - **Need dev and prod to *not* share** something (mutable fixed-key state, or sensitive uploads a developer shouldn't see)? There's no per-store isolation switch — scope the key yourself (e.g. `config/${env}/…`). Rare; the shared default is right almost always.
35
18
 
36
19
  ## Defining a store
37
20
 
38
- Like `db.defineTable`, define at module scope and import into methods. Access is pinned at define
39
- time.
21
+ Like `db.defineTable`, define at module scope and import into methods. Access is pinned at define time.
40
22
 
41
23
  ```typescript
42
24
  import { files } from '@mindstudio-ai/agent';
@@ -63,13 +45,9 @@ const { files, cursor } = await Uploads.list({ prefix: 'reports/', limit: 100 })
63
45
  await Uploads.delete(key);
64
46
  ```
65
47
 
66
- - `put(content, { key?, contentType?, filename?, contentAddressed? })` → `StoredFile`
67
- (`{ key, url, size?, contentType?, updatedAt?, shareUrl() }`). Omit `key` → UUID;
68
- `contentAddressed: true` → a `<sha256>.<ext>` key (immutable/idempotent, for baked-in public assets).
69
- - **`file.url`** is a plain relative string — don't await it. To display a user *their own* file, hand
70
- them `file.url`; don't `get()` the bytes and stream them yourself.
71
- - **`await file.shareUrl({ expiresIn })`** → an absolute signed link that works with **no session**
72
- (email / cross-site embed). Private stores only; default 24h.
48
+ - `put(content, { key?, contentType?, filename?, contentAddressed? })` → `StoredFile` (`{ key, url, size?, contentType?, updatedAt?, shareUrl() }`). Omit `key` → UUID; `contentAddressed: true` → a `<sha256>.<ext>` key (immutable/idempotent, for baked-in public assets).
49
+ - **`file.url`** is a plain relative string — don't await it. To display a user *their own* file, hand them `file.url`; don't `get()` the bytes and stream them yourself.
50
+ - **`await file.shareUrl({ expiresIn })`** → an absolute signed link that works with **no session** (email / cross-site embed). Private stores only; default 24h.
73
51
 
74
52
  ## User uploads (client-direct — bytes never go through the backend)
75
53
 
@@ -91,9 +69,7 @@ const { key, url } = await platform.upload(token, file, { onProgress: (f) => set
91
69
 
92
70
  ## Public assets + image resizing
93
71
 
94
- Public files are world-readable, served on the app's own domain, and **images resize via query
95
- params** — request the size you need rather than CSS-scaling a full-res original. Always set `dpr=2`
96
- or `3` when sizing so images stay crisp on Retina displays.
72
+ Public files are world-readable, served on the app's own domain, and **images resize via query params** — request the size you need rather than CSS-scaling a full-res original. Always set `dpr=2` or `3` when sizing so images stay crisp on Retina displays.
97
73
 
98
74
  | Param | Example | Effect |
99
75
  |-------|---------|--------|
@@ -109,13 +85,11 @@ or `3` when sizing so images stay crisp on Retina displays.
109
85
 
110
86
  Combine freely: `…/hero.jpg?w=200&h=200&fit=crop&fm=avif`.
111
87
 
112
- Lightweight-config pattern: a public store + a stable `key` is a file the frontend can `fetch` with no
113
- DB hit and the backend can overwrite (`Config.put(json, { key: 'config/latest.json' })`).
88
+ Lightweight-config pattern: a public store + a stable `key` is a file the frontend can `fetch` with no DB hit and the backend can overwrite (`Config.put(json, { key: 'config/latest.json' })`).
114
89
 
115
90
  ## Build-time / marketing assets
116
91
 
117
- Need an image on the site (hero, logo, OG image)? **Never commit binaries to the repo** — it bloats
118
- git. Upload once and embed the returned URL:
92
+ Need an image on the site (hero, logo, OG image)? **Never commit binaries to the repo** — it bloats git. Upload once and embed the returned URL:
119
93
 
120
94
  ```bash
121
95
  mindstudio-prod files put --public ./hero.jpg # → { url, key } — content-addressed, immutable
@@ -124,8 +98,7 @@ Write that URL into your JSX/HTML. Also: `files list`, `files rm --store … --k
124
98
 
125
99
  ## Generated assets
126
100
 
127
- MindStudio SDK Actions that produce a file (`generateImage`, `generateVideo`, `generateSpeech`, `generatePdf`,
128
- `upscaleImage`, …) can optionally write straight into a store — pass the handle as `store` in the options object:
101
+ MindStudio SDK Actions that produce a file (`generateImage`, `generateVideo`, `generateSpeech`, `generatePdf`, `upscaleImage`, …) can optionally write straight into a store — pass the handle as `store` in the options object:
129
102
 
130
103
  ```typescript
131
104
  const { imageUrl } = await mindstudio.generateImage({ prompt }, { store: Assets });
@@ -135,10 +108,7 @@ If omitted, files are written to the default global, public MindStudio CDN.
135
108
 
136
109
  ## When public vs private
137
110
 
138
- - **Private (default):** user uploads, generated docs, anything not world-readable. Reads are authed
139
- (the app session) or a short-lived `shareUrl`.
140
- - **Public:** marketing images, resizable media, config the frontend reads. Deliberate `access:
141
- 'public'`.
111
+ - **Private (default):** user uploads, generated docs, anything not world-readable. Reads are authed (the app session) or a short-lived `shareUrl`.
112
+ - **Public:** marketing images, resizable media, config the frontend reads. Deliberate `access: 'public'`.
142
113
 
143
- Per-user access is the app's job — key files per user (`{userId}/…`) and hand each user only their own
144
- URLs; the platform authorizes at the app level, not per file.
114
+ Per-user access is the app's job — key files per user (`{userId}/…`) and hand each user only their own URLs; the platform authorizes at the app level, not per file.
@@ -6,17 +6,13 @@ when: Before writing an email-handler method, adding an `email` interface, or pr
6
6
 
7
7
  # Inbound Email Interfaces
8
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.
9
+ 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.
11
10
 
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.
11
+ The addresses themselves are configured at the project level by the user through the Remy platform. Your job is the `interface.json` and the method that handles the mail, not domain registration or MX records.
15
12
 
16
13
  ## Address tiers
17
14
 
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.
15
+ 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.
20
16
 
21
17
  | Tier | Address | How it's set up |
22
18
  |---|---|---|
@@ -24,13 +20,9 @@ registration); the legacy tier is specific-localpart and frozen for new apps.
24
20
  | 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
21
  | 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
22
 
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.
23
+ 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@')) ...`). This is what makes per-purpose addresses free: you don't register `support@` anywhere, you just check for it.
30
24
 
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.
25
+ 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.
34
26
 
35
27
  ## Config (`interface.json`)
36
28
 
@@ -45,10 +37,7 @@ The top-level key must match the interface type (`email`):
45
37
  }
46
38
  ```
47
39
 
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.
40
+ `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.
52
41
 
53
42
  Declare it in `mindstudio.json`:
54
43
 
@@ -78,9 +67,7 @@ Declare it in `mindstudio.json`:
78
67
 
79
68
  ## Replying in thread
80
69
 
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`.
70
+ Replies go out through the SDK's `sendEmail` action (its full parameter list is in the SDK actions reference in your system prompt). Set `inReplyTo` to the incoming `messageId` and `references` to `[...references, messageId]`. Send to `replyTo` when it's set, otherwise `from`.
84
71
 
85
72
  ```typescript
86
73
  await mindstudio.sendEmail({
@@ -95,22 +82,14 @@ await mindstudio.sendEmail({
95
82
  });
96
83
  ```
97
84
 
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.
85
+ `sendEmail` returns `{ recipients, cc, bcc, from }` — who it sent to and the sender used. It does **not** return the sent message's own `Message-ID`, so thread off *inbound* mail, never off messages you sent.
101
86
 
102
87
  ## Attachments and size limits
103
88
 
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.
89
+ `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.
107
90
 
108
- Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by
109
- the platform before the method runs.
91
+ Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by the platform before the method runs.
110
92
 
111
93
  ## Auth
112
94
 
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.
95
+ Methods invoked through this interface run with `auth.roles: ['system']` — the platform is calling, not a user session, so there's no user to impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable via email. The auth reference in your system prompt covers the system role in full.
@@ -6,10 +6,7 @@ when: Before authoring `src/interfaces/mcp.md`, deciding which of the app's meth
6
6
 
7
7
  # MCP Interfaces
8
8
 
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.
9
+ Exposing an app as an MCP server — a tool / resource / prompt surface for *external* AI agents (Claude Desktop, Cursor, anyone's agent). Unlike the agent interface, which *is* an agent with its own LLM, personality and chat UI, MCP has no model of its own: it's the app projected as a server for an outside AI to drive.
13
10
 
14
11
  It supports the full MCP surface:
15
12
 
@@ -18,18 +15,11 @@ It supports the full MCP surface:
18
15
  - **Prompts** — reusable, parameterized prompt templates the server offers.
19
16
  - **Instructions** — server-level guidance shown to the calling agent (the toolset's "system prompt").
20
17
 
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.
18
+ The platform hosts the server, handles auth, and derives every tool's input schema from the method contract. So there's no protocol code to write, and the whole job is authorship plus a config file. Authorship first, since that's what decides whether the toolset actually works.
24
19
 
25
20
  ## The descriptions are the product
26
21
 
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.
22
+ 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.
33
23
 
34
24
  What that looks like in practice — the same method, described twice:
35
25
 
@@ -47,32 +37,22 @@ full updated vendor. Editors and admins only; other roles are rejected. For a
47
37
  vendor that doesn't exist yet, use `createVendor`.
48
38
  ```
49
39
 
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.
40
+ 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.
53
41
 
54
42
  ## Curate — not every method is a tool
55
43
 
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.
44
+ 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.
60
45
 
61
46
  ## Annotations
62
47
 
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:
48
+ Annotations are machine-readable hints clients use to decide whether to auto-call a tool or ask the user first. Set them honestly:
65
49
 
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.
50
+ - `readOnly` — the tool only reads, never mutates. The highest-value hint: clients auto-call reads without prompting, so set it on every pure read.
68
51
  - `destructive` — the tool can delete or overwrite. Clients gate these behind confirmation.
69
52
  - `idempotent` — calling twice with the same arguments has the same effect as calling once.
70
- - `openWorld` — the tool reaches outside the app (external web/services) rather than operating only on
71
- app data.
53
+ - `openWorld` — the tool reaches outside the app (external web/services) rather than operating only on app data.
72
54
 
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):
55
+ The judgement is per tool, and getting `readOnly` right is what makes a toolset feel responsive rather than nagging. Abbreviated to just the annotations (a real entry also carries `name`, `title` and `description` — the Config section has the full shape):
76
56
 
77
57
  ```jsonc
78
58
  "tools": [
@@ -84,32 +64,21 @@ than nagging. Abbreviated to just the annotations (a real entry also carries `na
84
64
  ]
85
65
  ```
86
66
 
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.
67
+ 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.
89
68
 
90
69
  ## Tools vs. resources
91
70
 
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.
71
+ 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.
95
72
 
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.
73
+ 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.
99
74
 
100
75
  ## Prompts
101
76
 
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.
77
+ 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.
105
78
 
106
79
  ## Server instructions
107
80
 
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.
81
+ 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.
113
82
 
114
83
  ```markdown
115
84
  This server exposes a procurement app. Vendors are the central record and
@@ -119,8 +88,7 @@ search tool before calling anything that takes one. Search results are capped
119
88
  at 50; page with the returned cursor rather than broadening the query.
120
89
  ```
121
90
 
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.
91
+ 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.
124
92
 
125
93
  ---
126
94
 
@@ -128,8 +96,7 @@ domain, so the calling agent's first move is a reasonable one.
128
96
 
129
97
  ## Spec: `src/interfaces/mcp.md`
130
98
 
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.
99
+ Frontmatter declares the server. In the body, the intro prose becomes the server `instructions`, and `## Tools`, `## Resources` and `## Prompts` headings declare the rest.
133
100
 
134
101
  ```yaml
135
102
  ---
@@ -252,8 +219,7 @@ The top-level key must match the interface type (`mcp`):
252
219
  | `prompts[].arguments` | `[{ name, description?, required? }]` |
253
220
  | `prompts[].template` | Relative path to the template body (`{{arg}}` placeholders) |
254
221
 
255
- There is no `inputSchema` field — the platform derives each tool's schema from the method's input
256
- contract.
222
+ There is no `inputSchema` field — the platform derives each tool's schema from the method's input contract.
257
223
 
258
224
  Declare it in `mindstudio.json`:
259
225
 
@@ -263,18 +229,10 @@ Declare it in `mindstudio.json`:
263
229
 
264
230
  ## Platform Behavior
265
231
 
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(...)`/`hasRole(...)` 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.
232
+ - The platform hosts the MCP server and exposes it to external clients. Clients connect at `POST https://{app-host}/_/mcp`, where `{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`).
233
+ - **Auth is optional.** A `Bearer` key resolves to a user with full RBAC, so the method's own `auth.requireRole(...)`/`hasRole(...)` checks apply as they would for that user. With no key, calls run anonymously — no user, no roles. The method is the boundary: gate sensitive tools, and understand that a public (keyless) server effectively exposes only the un-gated ones.
274
234
  - 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.
235
+ - `tools/list` is static; access is enforced per-method at call time (a gated tool is listed but rejects an unauthorized call).
236
+ - A resource read invokes the backing method (template `{param}`s come from the URI) and returns its output as the resource contents.
279
237
  - `prompts/get` fills the template with the provided arguments.
280
238
  - `instructions` is returned in the `initialize` response.
@@ -6,24 +6,15 @@ when: Before authoring `src/interfaces/api.md` or adding an `api` interface with
6
6
 
7
7
  # REST API Interfaces
8
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.
9
+ 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.
13
10
 
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.
11
+ 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.
16
12
 
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.
13
+ **For provider webhooks (Stripe, GitHub, Shopify) there are two native paths**, and they are easy to confuse. This interface handles them with bearer auth and `input._request.rawBody`. The Webhook interface handles them with secret-in-URL routing and a top-level `input.rawBody` — usually the better fit for provider callbacks, since providers can't send a bearer token. Load the `webhooks` skill before choosing.
22
14
 
23
15
  ## Spec: `src/interfaces/api.md`
24
16
 
25
- The human-readable spec. Frontmatter declares the API name and description; the body maps methods to
26
- REST routes using MSFM.
17
+ The human-readable spec. Frontmatter declares the API name and description; the body maps methods to REST routes using MSFM.
27
18
 
28
19
  ```yaml
29
20
  ---
@@ -33,8 +24,7 @@ type: interface/api
33
24
  ---
34
25
  ```
35
26
 
36
- Routes are declared as `VERB /path → methodExportName` under resource headings, with annotations for
37
- params and descriptions:
27
+ Routes are declared as `VERB /path → methodExportName` under resource headings, with annotations for params and descriptions:
38
28
 
39
29
  ```markdown
40
30
  ## Vendors
@@ -140,10 +130,6 @@ Routes are mounted at `/_/api{path}` (e.g. `DELETE /_/api/vendors/abc123`).
140
130
  - **Query params** are merged into input for GET requests: `?status=approved` → `{ status: "approved" }`
141
131
  - **Request body** for POST/PUT/PATCH is the input directly (no `{ input: {...} }` wrapper)
142
132
  - **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(...)`/`hasRole(...)` checks apply exactly as they would for that user
133
+ - **Auth** via `Authorization: Bearer sk_...` — an API key resolves to a user with full RBAC, so the method's own `auth.requireRole(...)`/`hasRole(...)` checks apply exactly as they would for that user
145
134
  - **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.
135
+ - **Raw request context**: Every API method receives `input._request` with `{ method, headers, rawBody }`. `rawBody` is the original unparsed body as a UTF-8 string — needed for signature verification, since providers HMAC the raw payload and a re-serialized body won't match. For most methods you don't need `_request` at all.
@@ -112,19 +112,15 @@ Scenarios are useful for seeding initial app state after build for testing, as w
112
112
 
113
113
  ## What scenarios don't touch
114
114
 
115
- **Scenarios seed database tables and nothing else.** They do not touch file stores or data sources —
116
- deliberately: both are durable and shared across dev and prod, with no per-release copy to reset, so
117
- there is nothing to truncate.
115
+ **Scenarios seed database tables and nothing else.** They do not touch file stores or data sources — deliberately: both are durable and shared across dev and prod, with no per-release copy to reset, so there is nothing to truncate.
118
116
 
119
- Don't try to seed documents into a data source from a scenario, and don't write `clear()`-style reset
120
- helpers for one. Load a test corpus once from the CLI instead:
117
+ Don't try to seed documents into a data source from a scenario, and don't write `clear()`-style reset helpers for one. Load a test corpus once from the CLI instead:
121
118
 
122
119
  ```bash
123
120
  mindstudio-prod datasources add --source policies --wait fixtures/*.pdf
124
121
  ```
125
122
 
126
- Re-running it is free (documents are content-addressed), so it's safe to keep in a setup script
127
- beside your scenarios.
123
+ Re-running it is free (documents are content-addressed), so it's safe to keep in a setup script beside your scenarios.
128
124
 
129
125
  ## Scenario Data
130
126
 
@@ -31,8 +31,7 @@ The top-level key must match the interface type (`cron`):
31
31
  }
32
32
  ```
33
33
 
34
- Standard cron expression format. `method` is the id of a method in `methods[]`. Jobs are synced to the
35
- platform on deploy.
34
+ Standard cron expression format. `method` is the id of a method in `methods[]`. Jobs are synced to the platform on deploy.
36
35
 
37
36
  Declare it in `mindstudio.json`:
38
37
 
@@ -42,10 +41,6 @@ Declare it in `mindstudio.json`:
42
41
 
43
42
  ## Auth
44
43
 
45
- Methods invoked through this interface run with `auth.roles: ['system']` — the platform is calling, not
46
- a user session, so there's no user to impersonate. Use `auth.requireRole('system')` to gate methods that
47
- should only be reachable on a schedule. The auth reference in your system prompt covers the system role
48
- in full.
44
+ Methods invoked through this interface run with `auth.roles: ['system']` — the platform is calling, not a user session, so there's no user to impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable on a schedule. The auth reference in your system prompt covers the system role in full.
49
45
 
50
- A scheduled job that needs to act on user data acts as the system, not as any user, so it reaches
51
- everything. Scope what it touches in the method itself rather than relying on role checks to narrow it.
46
+ A scheduled job that needs to act on user data acts as the system, not as any user, so it reaches everything. Scope what it touches in the method itself rather than relying on role checks to narrow it.