@mapled/mcp 0.1.0 → 0.9.0
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.
- package/README.md +31 -3
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +336 -5
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -4,6 +4,17 @@ MCP server for [Mapled](https://mapled.io) — a hosted headless CMS built for s
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
+
The quickest way is the hosted endpoint with OAuth — no token to copy. In Claude Code:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add --transport http mapled https://api.mapled.io/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Your browser opens Mapled, you pick the project and authorize; the connection appears in Integrations and can be revoked there. Any MCP client that supports remote servers with OAuth (Claude Desktop, Cursor, ChatGPT) works the same way.
|
|
14
|
+
|
|
15
|
+
The local stdio server below is the alternative for clients without OAuth support.
|
|
16
|
+
|
|
17
|
+
|
|
7
18
|
Create a token in Mapled: project → Integrations → **Connect with AI**. Then add the server to your agent's MCP config:
|
|
8
19
|
|
|
9
20
|
```json
|
|
@@ -26,17 +37,34 @@ claude mcp add mapled -e MAPLED_MCP_TOKEN=mcp_live_… -- npx -y @mapled/mcp
|
|
|
26
37
|
|
|
27
38
|
The token is scoped to one project. Set `MAPLED_API_URL` only if you're not on the default `https://api.mapled.io`.
|
|
28
39
|
|
|
40
|
+
## Setting up a site
|
|
41
|
+
|
|
42
|
+
For the first integration, propose one plan and let the owner approve it on a trusted Mapled screen:
|
|
43
|
+
|
|
44
|
+
1. `get_schema` — see what already exists.
|
|
45
|
+
2. `propose_setup_plan` — the collections and singles (with fields and the records to import), the files you will change, the packages you will install. Nothing changes yet. Relation fields name their target — a collection of the plan by its display name, or an existing one by key; give a record a `"$ref": "jane"` and other records of the plan link to it as `"author": "jane"` (a list of refs for `many`), while links to existing collections use record ids from `list_records`. Group fields carry their sub-fields; their values are objects keyed by the sub-field keys. Mark a field — or a sub-field of a group — `sensitive: true` when editors keep it but the site must never get it. A link that does not resolve is answered right away with its path, so fix the plan before the owner sees it.
|
|
46
|
+
3. Ask the user to open the returned `reviewUrl` and approve. Poll `get_setup_run` until its status is `approved` (or `rejected` — then propose a better plan).
|
|
47
|
+
4. `apply_setup_plan` — Mapled creates everything in one go and tells you the keys it assigned.
|
|
48
|
+
5. Wire the site: `get_connection`, `configure_revalidation`, deploy, then close with `report_setup` (files changed, `buildPassed`, `secretsCommitted: false`). Mapled runs its own checks — the site reads content, the webhook delivered, the preview route responds, fields have help texts — and the run is completed only when they pass. Fix what failed and call `verify_setup`.
|
|
49
|
+
|
|
50
|
+
## Destructive changes
|
|
51
|
+
|
|
52
|
+
`delete_collection`, `remove_field` and `clear_records` never act on their own. Each one files a request and returns a `reviewUrl`: send it to the user, who confirms on a trusted Mapled screen by typing the name. Poll `get_confirmation` until the status is `applied` (the result says what changed), `denied` or `expired` (after an hour). Nothing is deleted until then.
|
|
53
|
+
|
|
29
54
|
## Tools
|
|
30
55
|
|
|
31
56
|
| Tool | What it does |
|
|
32
57
|
| --- | --- |
|
|
58
|
+
| `propose_setup_plan` / `get_setup_run` / `apply_setup_plan` / `report_setup` / `verify_setup` | One approved plan for the whole setup — every field type including relations (`$ref` handles between plan records, ids for existing collections) and groups — verified by Mapled (see above) |
|
|
33
59
|
| `get_schema` | Read the project's collections and fields |
|
|
34
60
|
| `create_collection` | Add a collection or single |
|
|
35
|
-
| `add_field` | Add a field (short_text, long_text, rich_text, slug, image, number, boolean, date) |
|
|
61
|
+
| `add_field` | Add a field (short_text, long_text, rich_text, slug, image, number, boolean, date, datetime, relation, enum, url, email, group, file, color, json — an object or list up to 32 KB, location — { lat, lng }). Optional `validation` ({min, max, pattern}) and `defaultValue`; `relation` ({target, cardinality: one \| many, onDelete: restrict \| nullify}) is required for relation fields — values are record ids of the target collection, kept in the order given; `onDelete` says what a delete of a linked record does (restrict: it can't be deleted while linked, nullify: the links are cleared — the default is restrict for required fields and nullify otherwise); `options` (1–50 labels) is required for enum fields; `sensitive: true` keeps a field out of lists, history and delivery (a group's sub-fields take it too); `group` ({fields, repeatable, maxItems}) shapes a group field — its values are objects (or arrays of them) keyed by the sub-field keys. |
|
|
36
62
|
| `add_records` | Insert draft records |
|
|
37
|
-
| `list_records` | Read a collection's draft records |
|
|
63
|
+
| `list_records` | Read a collection's draft records, newest edit first — `query` searches their content, `limit` (1–200) and `cursor` (the previous answer's `nextCursor`) page through them; `total` counts every match |
|
|
38
64
|
| `create_form` / `list_forms` | Set up public forms with spam protection |
|
|
39
|
-
| `get_connection` | Delivery key + API URL for wiring the site (`@mapled/next`) |
|
|
65
|
+
| `get_connection` (image values → `assetUrl(id, { width })` from @mapled/next) | Delivery key + API URL for wiring the site (`@mapled/next`) |
|
|
66
|
+
| `push_site_manifest` / `list_bindings` | Tell Mapled where each field is rendered; read every binding's health (type mismatch, outdated, missing on site) |
|
|
40
67
|
| `configure_revalidation` | Point the publish webhook at the site, get the signing secret |
|
|
68
|
+
| `check_integration` | The site's integration as Mapled sees it — delivery reads, the webhook and its last delivery, the bindings summary, current package versions; `npx mapled doctor` shows the same from inside the repository |
|
|
41
69
|
|
|
42
70
|
Agents write drafts only — publishing stays with humans in the Mapled app, and nothing changes on the live site until someone presses Publish.
|
package/dist/tools.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type ApiClient = {
|
|
|
5
5
|
request: (method: "GET" | "POST" | "PATCH", path: string, body?: unknown) => Promise<unknown>;
|
|
6
6
|
};
|
|
7
7
|
export declare function createApiClient(baseUrl: string, token: string): ApiClient;
|
|
8
|
-
export declare const FIELD_TYPES: readonly ["short_text", "long_text", "rich_text", "slug", "image", "number", "boolean", "date"];
|
|
8
|
+
export declare const FIELD_TYPES: readonly ["short_text", "long_text", "rich_text", "slug", "image", "number", "boolean", "date", "relation", "enum", "url", "email", "group", "datetime", "file", "color", "json", "location"];
|
|
9
9
|
export type ToolDef = {
|
|
10
10
|
name: string;
|
|
11
11
|
description: string;
|
package/dist/tools.js
CHANGED
|
@@ -27,6 +27,16 @@ export const FIELD_TYPES = [
|
|
|
27
27
|
"number",
|
|
28
28
|
"boolean",
|
|
29
29
|
"date",
|
|
30
|
+
"relation",
|
|
31
|
+
"enum",
|
|
32
|
+
"url",
|
|
33
|
+
"email",
|
|
34
|
+
"group",
|
|
35
|
+
"datetime",
|
|
36
|
+
"file",
|
|
37
|
+
"color",
|
|
38
|
+
"json",
|
|
39
|
+
"location",
|
|
30
40
|
];
|
|
31
41
|
export function createTools(api) {
|
|
32
42
|
return [
|
|
@@ -36,38 +46,331 @@ export function createTools(api) {
|
|
|
36
46
|
schema: {},
|
|
37
47
|
handler: async () => api.request("GET", "/v1/agent/schema"),
|
|
38
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: "propose_setup_plan",
|
|
51
|
+
description: "Propose the whole setup of a site in one plan: the collections and singles to create (with fields and the " +
|
|
52
|
+
"records to import), the files you will change and the packages you will install. The plan waits for the " +
|
|
53
|
+
"project's builder to approve it on a trusted Mapled screen — nothing changes until then. Use this for the " +
|
|
54
|
+
"initial integration instead of creating collections one by one. Record keys are the field keys: the " +
|
|
55
|
+
"lowercase, hyphenated display name (\"Show badge\" → \"show-badge\"). Relation fields name their target " +
|
|
56
|
+
"collection (one of this plan by its display name, or an existing one by key); give a record a \"$ref\" " +
|
|
57
|
+
"(a short name) and other records of the plan link to it by that name — links to existing collections use " +
|
|
58
|
+
"record ids. Group fields carry their sub-fields; their values are objects keyed by the sub-field keys. " +
|
|
59
|
+
"Returns the run id and reviewUrl: ask the user to open it and approve, then poll get_setup_run until the " +
|
|
60
|
+
"status is \"approved\" and call apply_setup_plan.",
|
|
61
|
+
schema: {
|
|
62
|
+
summary: z.string().min(1).max(500),
|
|
63
|
+
collections: z
|
|
64
|
+
.array(z.object({
|
|
65
|
+
displayName: z.string().min(1).max(120),
|
|
66
|
+
kind: z.enum(["collection", "single"]).optional(),
|
|
67
|
+
mode: z.enum(["editorial", "operational"]).optional(),
|
|
68
|
+
accessClass: z.enum(["public", "server"]).optional(),
|
|
69
|
+
fields: z
|
|
70
|
+
.array(z.object({
|
|
71
|
+
displayName: z.string().min(1).max(120),
|
|
72
|
+
type: z.enum(FIELD_TYPES),
|
|
73
|
+
required: z.boolean().optional(),
|
|
74
|
+
helpText: z.string().max(500).optional(),
|
|
75
|
+
sensitive: z
|
|
76
|
+
.boolean()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Masked in lists and history and never delivered to the site — for data editors keep but the site must not show. A group takes it on its sub-fields instead."),
|
|
79
|
+
options: z
|
|
80
|
+
.array(z.string().min(1).max(60))
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(50)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("For type enum only: the values a record may hold, in display order."),
|
|
85
|
+
relation: z
|
|
86
|
+
.object({
|
|
87
|
+
target: z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.max(120)
|
|
91
|
+
.describe("The display name of a collection in this plan, or the key of a collection the project already has (see get_schema). Singles can't be targets."),
|
|
92
|
+
cardinality: z
|
|
93
|
+
.enum(["one", "many"])
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("one (default): the record links to one record; many: to a list of them, in the order given."),
|
|
96
|
+
onDelete: z
|
|
97
|
+
.enum(["restrict", "nullify"])
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("What a delete of a linked record does: restrict — it can't be deleted while linked; nullify — the links are cleared. Default: restrict when the field is required, nullify otherwise."),
|
|
100
|
+
})
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("For type relation only. In records the value is the $ref of a record in this plan (an array of $refs for many), or record ids when the target collection already exists."),
|
|
103
|
+
group: z
|
|
104
|
+
.object({
|
|
105
|
+
fields: z
|
|
106
|
+
.array(z.object({
|
|
107
|
+
displayName: z.string().min(1).max(120),
|
|
108
|
+
type: z.enum(["short_text", "long_text", "rich_text", "image", "number", "boolean", "date", "url", "email", "enum", "datetime", "file", "color", "json", "location"]),
|
|
109
|
+
required: z.boolean().optional(),
|
|
110
|
+
helpText: z.string().max(500).optional(),
|
|
111
|
+
options: z.array(z.string().min(1).max(60)).min(1).max(50).optional().describe("For sub-field type enum only."),
|
|
112
|
+
sensitive: z
|
|
113
|
+
.boolean()
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("Masked in lists and history and never delivered to the site — this sub-field's values stay with the editors."),
|
|
116
|
+
}))
|
|
117
|
+
.min(1)
|
|
118
|
+
.max(20),
|
|
119
|
+
repeatable: z.boolean().optional().describe("true (default): a list of items; false: one nested object"),
|
|
120
|
+
maxItems: z.number().int().min(1).max(100).optional().describe("Repeatable groups only."),
|
|
121
|
+
})
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("For type group only. In records the value is an object keyed by the sub-field keys (lowercase, hyphenated names) — or an array of such objects when repeatable."),
|
|
124
|
+
}))
|
|
125
|
+
.min(1)
|
|
126
|
+
.max(40),
|
|
127
|
+
records: z
|
|
128
|
+
.array(z.record(z.string(), z.unknown()))
|
|
129
|
+
.max(100)
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Records keyed by field key. Add \"$ref\": \"<short name>\" to a record so relation fields of other records in the plan can link to it; $ref is not stored."),
|
|
132
|
+
}))
|
|
133
|
+
.min(1)
|
|
134
|
+
.max(30),
|
|
135
|
+
files: z.array(z.string().max(300)).max(50).optional(),
|
|
136
|
+
packages: z.array(z.string().max(120)).max(20).optional(),
|
|
137
|
+
notes: z.array(z.string().max(300)).max(20).optional(),
|
|
138
|
+
},
|
|
139
|
+
handler: async (args) => api.request("POST", "/v1/agent/runs", args),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "get_setup_run",
|
|
143
|
+
description: "Check a setup run: status is awaiting_approval, approved, rejected, applying, completed, " +
|
|
144
|
+
"completed_with_warnings or failed. Poll it after propose_setup_plan; apply once it is approved.",
|
|
145
|
+
schema: { runId: z.string().uuid() },
|
|
146
|
+
handler: async (args) => api.request("GET", `/v1/agent/runs/${encodeURIComponent(args.runId)}`),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "apply_setup_plan",
|
|
150
|
+
description: "Apply an approved setup plan. Mapled creates the collections, fields and records in one go and returns " +
|
|
151
|
+
"what was created (keys, counts) plus warnings for records it had to skip. Then wire the site " +
|
|
152
|
+
"(get_connection, configure_revalidation) and finish with report_setup.",
|
|
153
|
+
schema: { runId: z.string().uuid() },
|
|
154
|
+
handler: async (args) => api.request("POST", `/v1/agent/runs/${encodeURIComponent(args.runId)}/apply`, {}),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "report_setup",
|
|
158
|
+
description: "Close a setup run with your report: the files you changed, what stayed hardcoded, warnings the owner " +
|
|
159
|
+
"should know about, whether the site builds (buildPassed) and that no secrets were committed " +
|
|
160
|
+
"(secretsCommitted: false), plus the preview URL if the site is deployed. Mapled then runs its own " +
|
|
161
|
+
"checks (site reads content, webhook delivered, preview route responds, help texts) and returns them; " +
|
|
162
|
+
"the run is completed only when every check passes — fix what failed and call verify_setup.",
|
|
163
|
+
schema: {
|
|
164
|
+
runId: z.string().uuid(),
|
|
165
|
+
filesChanged: z.array(z.string().max(300)).max(50).optional(),
|
|
166
|
+
stillHardcoded: z.array(z.string().max(300)).max(50).optional(),
|
|
167
|
+
warnings: z.array(z.string().max(300)).max(20).optional(),
|
|
168
|
+
previewUrl: z.string().url().optional(),
|
|
169
|
+
buildPassed: z.boolean().optional(),
|
|
170
|
+
secretsCommitted: z.boolean().optional(),
|
|
171
|
+
},
|
|
172
|
+
handler: async (args) => {
|
|
173
|
+
const { runId, ...body } = args;
|
|
174
|
+
return api.request("POST", `/v1/agent/runs/${encodeURIComponent(runId)}/report`, body);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "verify_setup",
|
|
179
|
+
description: "Re-run Mapled's server checks on a setup run after fixing something: the site must read content with " +
|
|
180
|
+
"the delivery key, the publish webhook must have delivered, /api/mapled/preview must respond, and the " +
|
|
181
|
+
"created fields should have help texts. Returns each check with a detail line.",
|
|
182
|
+
schema: { runId: z.string().uuid() },
|
|
183
|
+
handler: async (args) => api.request("POST", `/v1/agent/runs/${encodeURIComponent(args.runId)}/verify`, {}),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "delete_collection",
|
|
187
|
+
description: "Destructive: delete a collection or single with all its fields and records. Nothing is deleted until a " +
|
|
188
|
+
"person confirms it on a trusted Mapled screen by typing its name. Returns the request with a reviewUrl — " +
|
|
189
|
+
"send it to the user — then poll get_confirmation until the status is applied, denied or expired.",
|
|
190
|
+
schema: { collectionKey: z.string().min(1).max(120) },
|
|
191
|
+
handler: async (args) => api.request("POST", "/v1/agent/confirmations", {
|
|
192
|
+
action: "delete_collection",
|
|
193
|
+
collectionKey: args.collectionKey,
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "remove_field",
|
|
198
|
+
description: "Destructive: remove a field from a collection; its values in every record are dropped. Needs a person's " +
|
|
199
|
+
"confirmation on a trusted Mapled screen (returns a reviewUrl to send to the user); poll get_confirmation.",
|
|
200
|
+
schema: { collectionKey: z.string().min(1).max(120), fieldKey: z.string().min(1).max(120) },
|
|
201
|
+
handler: async (args) => api.request("POST", "/v1/agent/confirmations", {
|
|
202
|
+
action: "delete_field",
|
|
203
|
+
collectionKey: args.collectionKey,
|
|
204
|
+
fieldKey: args.fieldKey,
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "clear_records",
|
|
209
|
+
description: "Destructive: move every record of a collection to Trash (kept 7 days), e.g. before re-importing content. " +
|
|
210
|
+
"Needs a person's confirmation on a trusted Mapled screen (returns a reviewUrl to send to the user); poll " +
|
|
211
|
+
"get_confirmation.",
|
|
212
|
+
schema: { collectionKey: z.string().min(1).max(120) },
|
|
213
|
+
handler: async (args) => api.request("POST", "/v1/agent/confirmations", {
|
|
214
|
+
action: "clear_records",
|
|
215
|
+
collectionKey: args.collectionKey,
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "get_confirmation",
|
|
220
|
+
description: "Check a destructive-change request: status is pending (waiting for the person), applied (done — result " +
|
|
221
|
+
"says what changed), denied, expired (after an hour; request again if still needed) or failed.",
|
|
222
|
+
schema: { confirmationId: z.string().uuid() },
|
|
223
|
+
handler: async (args) => api.request("GET", `/v1/agent/confirmations/${encodeURIComponent(args.confirmationId)}`),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "push_site_manifest",
|
|
227
|
+
description: "Tell Mapled where the site renders each field: one binding per place (a stable key, the page route, " +
|
|
228
|
+
"the component and file, the collection and field, and the target kind — text, rich_text, image, " +
|
|
229
|
+
"image_alt, link, number, date, boolean, collection for a repeated list, route_param, form_field). " +
|
|
230
|
+
"Push the full list every time; bindings you leave out are marked missing on the site. Mapled grades " +
|
|
231
|
+
"each binding against the schema (healthy, type mismatch, outdated) and returns warnings for unknown " +
|
|
232
|
+
"collections or fields. Needs the builder plan.",
|
|
233
|
+
schema: {
|
|
234
|
+
framework: z.string().max(40).optional(),
|
|
235
|
+
pages: z.array(z.object({ route: z.string().max(200), file: z.string().max(300).optional() })).max(200).optional(),
|
|
236
|
+
bindings: z
|
|
237
|
+
.array(z.object({
|
|
238
|
+
key: z.string().min(1).max(120),
|
|
239
|
+
page: z.string().min(1).max(200),
|
|
240
|
+
component: z.string().max(120).optional(),
|
|
241
|
+
file: z.string().max(300).optional(),
|
|
242
|
+
collection: z.string().min(1).max(120),
|
|
243
|
+
field: z.string().min(1).max(120).optional(),
|
|
244
|
+
target: z.enum([
|
|
245
|
+
"text",
|
|
246
|
+
"rich_text",
|
|
247
|
+
"image",
|
|
248
|
+
"image_alt",
|
|
249
|
+
"link",
|
|
250
|
+
"number",
|
|
251
|
+
"date",
|
|
252
|
+
"boolean",
|
|
253
|
+
"collection",
|
|
254
|
+
"route_param",
|
|
255
|
+
"form_field",
|
|
256
|
+
"other",
|
|
257
|
+
]),
|
|
258
|
+
required: z.boolean().optional(),
|
|
259
|
+
}))
|
|
260
|
+
.max(500),
|
|
261
|
+
notes: z.array(z.string().max(300)).max(20).optional(),
|
|
262
|
+
},
|
|
263
|
+
handler: async (args) => api.request("POST", "/v1/agent/manifest", args),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "list_bindings",
|
|
267
|
+
description: "Read the bindings with their health: healthy, missing_on_site, type_mismatch, outdated (the field or " +
|
|
268
|
+
"collection changed since the manifest), not_checked, disabled. Fix the site or the schema, then push " +
|
|
269
|
+
"the manifest again.",
|
|
270
|
+
schema: {},
|
|
271
|
+
handler: async () => api.request("GET", "/v1/agent/bindings"),
|
|
272
|
+
},
|
|
39
273
|
{
|
|
40
274
|
name: "create_collection",
|
|
41
|
-
description: "Create a collection (many records of one shape) or a single (exactly one record, e.g. a homepage)."
|
|
275
|
+
description: "Create a collection (many records of one shape) or a single (exactly one record, e.g. a homepage). " +
|
|
276
|
+
"For the initial setup of a site prefer propose_setup_plan so the owner approves everything at once. " +
|
|
277
|
+
"mode: 'editorial' (default — drafts, humans publish) or 'operational' (Live data — saves take " +
|
|
278
|
+
"effect immediately; the site reads AND writes it through the Application Data API with the " +
|
|
279
|
+
"server key, e.g. orders, counters, sign-ups). The mode is fixed at creation.",
|
|
42
280
|
schema: {
|
|
43
281
|
displayName: z.string().min(1).max(120),
|
|
44
282
|
kind: z.enum(["collection", "single"]).optional(),
|
|
283
|
+
mode: z.enum(["editorial", "operational"]).optional(),
|
|
45
284
|
},
|
|
46
285
|
handler: async (args) => api.request("POST", "/v1/agent/collections", {
|
|
47
286
|
displayName: args.displayName,
|
|
48
287
|
kind: args.kind ?? "collection",
|
|
288
|
+
mode: args.mode ?? "editorial",
|
|
49
289
|
}),
|
|
50
290
|
},
|
|
51
291
|
{
|
|
52
292
|
name: "add_field",
|
|
53
|
-
description: "Add a field to a collection. Types: short_text, long_text, rich_text, slug, image, number, boolean, date.",
|
|
293
|
+
description: "Add a field to a collection. Types: short_text, long_text, rich_text (Markdown: headings, lists, links, bold/italic, images as ), slug, image, number, boolean, date, relation (a link to records of another collection: pass `relation`; values are record ids), enum (a choice: pass `options`), url, email, group (an object shaped by its own `group.fields`, or a list of them when repeatable — feature cards, FAQ items; values are objects / arrays of objects keyed by the sub-field keys), datetime (ISO 8601, stored in UTC), file (an asset id of any uploaded file), color (#rrggbb), json (any object or list up to 32 KB — settings, specs, structured data the site reads as is), location ({ lat, lng } in degrees).",
|
|
54
294
|
schema: {
|
|
55
295
|
collectionKey: z.string().min(1).max(120),
|
|
56
296
|
displayName: z.string().min(1).max(120),
|
|
57
297
|
type: z.enum(FIELD_TYPES),
|
|
58
298
|
required: z.boolean().optional(),
|
|
59
299
|
helpText: z.string().max(500).optional(),
|
|
300
|
+
validation: z
|
|
301
|
+
.object({
|
|
302
|
+
min: z.union([z.number(), z.string()]).optional(),
|
|
303
|
+
max: z.union([z.number(), z.string()]).optional(),
|
|
304
|
+
pattern: z.string().max(200).optional(),
|
|
305
|
+
})
|
|
306
|
+
.optional()
|
|
307
|
+
.describe("Rules checked on every save: min/max are a character count for text, a value for numbers, a YYYY-MM-DD day for dates; pattern (regex) applies to short_text and slug only."),
|
|
308
|
+
defaultValue: z
|
|
309
|
+
.unknown()
|
|
310
|
+
.optional()
|
|
311
|
+
.describe("Value a new record starts with when the field is not given."),
|
|
312
|
+
relation: z
|
|
313
|
+
.object({
|
|
314
|
+
target: z.string().min(1).max(120).describe("Key of the related collection."),
|
|
315
|
+
cardinality: z
|
|
316
|
+
.enum(["one", "many"])
|
|
317
|
+
.optional()
|
|
318
|
+
.describe("one (default): the value is a record id; many: an array of record ids (their order is kept)."),
|
|
319
|
+
onDelete: z
|
|
320
|
+
.enum(["restrict", "nullify"])
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("What a delete of a linked record does: restrict — the linked record can't be deleted while links to it exist; nullify — the links are cleared. Default: restrict when the field is required, nullify otherwise."),
|
|
323
|
+
})
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Required for type relation, not allowed otherwise."),
|
|
326
|
+
options: z
|
|
327
|
+
.array(z.string().min(1).max(60))
|
|
328
|
+
.min(1)
|
|
329
|
+
.max(50)
|
|
330
|
+
.optional()
|
|
331
|
+
.describe("For type enum only: the values a record may hold, in display order."),
|
|
332
|
+
sensitive: z
|
|
333
|
+
.boolean()
|
|
334
|
+
.optional()
|
|
335
|
+
.describe("Masked in lists and history and never delivered to the site — for data editors keep but the site must not show. A group takes it on its sub-fields instead."),
|
|
336
|
+
group: z
|
|
337
|
+
.object({
|
|
338
|
+
fields: z
|
|
339
|
+
.array(z.object({
|
|
340
|
+
displayName: z.string().min(1).max(120),
|
|
341
|
+
type: z.enum(["short_text", "long_text", "rich_text", "image", "number", "boolean", "date", "url", "email", "enum", "datetime", "file", "color", "json", "location"]),
|
|
342
|
+
required: z.boolean().optional(),
|
|
343
|
+
helpText: z.string().max(500).optional(),
|
|
344
|
+
options: z.array(z.string().min(1).max(60)).min(1).max(50).optional(),
|
|
345
|
+
sensitive: z
|
|
346
|
+
.boolean()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe("Masked in lists and history and never delivered to the site — this sub-field's values stay with the editors."),
|
|
349
|
+
}))
|
|
350
|
+
.min(1)
|
|
351
|
+
.max(20),
|
|
352
|
+
repeatable: z.boolean().optional().describe("true (default): a list of items; false: one nested object"),
|
|
353
|
+
maxItems: z.number().int().min(1).max(100).optional(),
|
|
354
|
+
})
|
|
355
|
+
.optional()
|
|
356
|
+
.describe("For type group only. Sub-field keys are the lowercase, hyphenated names."),
|
|
60
357
|
},
|
|
61
358
|
handler: async (args) => api.request("POST", `/v1/agent/collections/${encodeURIComponent(args.collectionKey)}/fields`, {
|
|
62
359
|
displayName: args.displayName,
|
|
63
360
|
type: args.type,
|
|
64
361
|
required: args.required ?? false,
|
|
65
362
|
helpText: args.helpText,
|
|
363
|
+
...(args.validation ? { validation: args.validation } : {}),
|
|
364
|
+
...(args.defaultValue !== undefined ? { defaultValue: args.defaultValue } : {}),
|
|
365
|
+
...(args.relation ? { relation: args.relation } : {}),
|
|
366
|
+
...(args.options ? { options: args.options } : {}),
|
|
367
|
+
...(args.sensitive !== undefined ? { sensitive: args.sensitive } : {}),
|
|
368
|
+
...(args.group ? { group: args.group } : {}),
|
|
66
369
|
}),
|
|
67
370
|
},
|
|
68
371
|
{
|
|
69
372
|
name: "add_records",
|
|
70
|
-
description: "Insert up to 100 records into a collection. Each record maps field keys to values.",
|
|
373
|
+
description: "Insert up to 100 records into a collection. Each record maps field keys to values (rich_text takes Markdown; image takes an asset id; relation takes record ids; group takes objects).",
|
|
71
374
|
schema: {
|
|
72
375
|
collectionKey: z.string().min(1).max(120),
|
|
73
376
|
records: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
|
|
@@ -95,17 +398,35 @@ export function createTools(api) {
|
|
|
95
398
|
},
|
|
96
399
|
{
|
|
97
400
|
name: "list_records",
|
|
98
|
-
description: "List a collection's draft records (id, title, data).",
|
|
401
|
+
description: "List a collection's draft records (id, title, data), most recently edited first — up to `limit` per page (200 by default). `query` narrows the list to records whose content contains it; when the answer carries nextCursor, pass it as `cursor` for the next page. `total` counts every match.",
|
|
99
402
|
schema: {
|
|
100
403
|
collectionKey: z.string().min(1).max(120),
|
|
404
|
+
query: z.string().max(200).optional().describe("Text to search for in the records' content."),
|
|
405
|
+
limit: z.number().int().min(1).max(200).optional().describe("Records per page, 1–200 (default 200)."),
|
|
406
|
+
cursor: z.string().max(200).optional().describe("nextCursor from the previous page."),
|
|
407
|
+
},
|
|
408
|
+
handler: async (args) => {
|
|
409
|
+
const params = new URLSearchParams();
|
|
410
|
+
if (args.query)
|
|
411
|
+
params.set("q", args.query);
|
|
412
|
+
if (args.limit !== undefined)
|
|
413
|
+
params.set("limit", String(args.limit));
|
|
414
|
+
if (args.cursor)
|
|
415
|
+
params.set("cursor", args.cursor);
|
|
416
|
+
const qs = params.toString();
|
|
417
|
+
return api.request("GET", `/v1/agent/collections/${encodeURIComponent(args.collectionKey)}/records${qs ? `?${qs}` : ""}`);
|
|
101
418
|
},
|
|
102
|
-
handler: async (args) => api.request("GET", `/v1/agent/collections/${encodeURIComponent(args.collectionKey)}/records`),
|
|
103
419
|
},
|
|
104
420
|
{
|
|
105
421
|
name: "get_connection",
|
|
106
422
|
description: "Get what the site needs to read published Mapled content: the delivery key and API URL. " +
|
|
107
423
|
"Wire-up: npm install @mapled/next, put the key in the site's env as MAPLED_KEY, then " +
|
|
108
424
|
'createClient({ key: process.env.MAPLED_KEY! }).getRecords("<collection>") in server components. ' +
|
|
425
|
+
"Reads take filter ({ field: value } or { field: { gte, lt, in, contains… } }), sort, limit/offset, " +
|
|
426
|
+
"fields, and expand (relation fields, e.g. [\"author\"]) — linked records arrive under record.expanded; " +
|
|
427
|
+
"getRecordBySlug(collection, slug) reads one record by its slug field. " +
|
|
428
|
+
"Image values are asset ids: assetUrl(id, { width, format }) from @mapled/next gives the URL of a resized variant. " +
|
|
429
|
+
"rich_text values are Markdown — render them with a Markdown component (e.g. react-markdown), never as raw HTML. " +
|
|
109
430
|
"Content appears on the site only after a human presses Publish in Mapled.",
|
|
110
431
|
schema: {},
|
|
111
432
|
handler: async () => api.request("GET", "/v1/agent/connection"),
|
|
@@ -122,5 +443,15 @@ export function createTools(api) {
|
|
|
122
443
|
},
|
|
123
444
|
handler: async (args) => api.request("PATCH", "/v1/agent/webhook", { url: args.url }),
|
|
124
445
|
},
|
|
446
|
+
{
|
|
447
|
+
name: "check_integration",
|
|
448
|
+
description: "See the site's integration as Mapled sees it: whether the site has read content with the delivery key " +
|
|
449
|
+
"and when, the publish webhook's URL and its last delivery, the bindings summary of the last manifest, " +
|
|
450
|
+
"the connection you hold and the package versions Mapled considers current (@mapled/next, @mapled/mcp, " +
|
|
451
|
+
"the mapled CLI). Use it to find what is left to wire up; `npx mapled doctor` shows the same picture " +
|
|
452
|
+
"from inside the repository, plus the local checks (env, routes, generated types, secrets in git).",
|
|
453
|
+
schema: {},
|
|
454
|
+
handler: async () => api.request("GET", "/v1/agent/integration"),
|
|
455
|
+
},
|
|
125
456
|
];
|
|
126
457
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Mapled MCP server: lets AI agents build schema and content for one Mapled project.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mapled.io",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
28
|
"test": "vitest run",
|
|
29
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
30
|
+
"prepare": "npm run build"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -35,5 +36,10 @@
|
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"typescript": "^5.8.0",
|
|
37
38
|
"vitest": "^3.2.0"
|
|
38
|
-
}
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./dist/index.js",
|
|
42
|
+
"./tools": "./dist/tools.js"
|
|
43
|
+
},
|
|
44
|
+
"types": "dist/index.d.ts"
|
|
39
45
|
}
|