@mapled/mcp 0.17.1 → 0.18.1
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 +4 -2
- package/dist/index.js +4 -23
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +162 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ claude mcp add mapled -e MAPLED_MCP_TOKEN=mcp_live_… -- npx -y @mapled/mcp
|
|
|
38
38
|
|
|
39
39
|
The token is scoped to one project. Set `MAPLED_API_URL` only if you're not on the default `https://api.mapled.io`.
|
|
40
40
|
|
|
41
|
+
Over either transport, a refusal reaches the agent as Mapled words it — a key that is taken, a plan still waiting for approval, the slug it sent that another record already has. An error whose text carries anything shaped like a Mapled key or token that the call didn't send itself reaches it as one constant sentence instead: no tool answer, its errors included, hands the agent a secret.
|
|
42
|
+
|
|
41
43
|
## Setting up a site
|
|
42
44
|
|
|
43
45
|
For the first integration, propose one plan and let the owner approve it on a trusted Mapled screen:
|
|
@@ -46,7 +48,7 @@ For the first integration, propose one plan and let the owner approve it on a tr
|
|
|
46
48
|
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.
|
|
47
49
|
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).
|
|
48
50
|
4. `apply_setup_plan` — Mapled creates everything in one go and tells you the keys it assigned.
|
|
49
|
-
5. Wire the site: `get_connection`, then `configure_revalidation` for a site with a server — or `set_site_url` for one rendered in the browser (a React single-page app, plain HTML: no webhook, no preview route) — and deploy.
|
|
51
|
+
5. Wire the site: `get_connection`, then `configure_revalidation` for a site with a server — or `set_site_url` for one rendered in the browser (a React single-page app, plain HTML: no webhook, no preview route) — and deploy. The webhook's signing secret never passes through the agent: the owner takes it from Mapled → Integrations → Your site, where Rotate secret shows a new one once, into the site's env as `MAPLED_WEBHOOK_SECRET`.
|
|
50
52
|
6. Leave a guide: `get_mapled_md` renders `MAPLED.md` from the project — what the site reads and where, the content model, the working rules, the commands that verify the integration. Write it to the repository root and commit it; the next agent (or person) starts from it. When the file exists, replace everything above its `<!-- mapled:notes -->` line and keep the notes below.
|
|
51
53
|
7. Close with `report_setup` (files changed, `buildPassed`, `secretsCommitted: false`, `mapledMdWritten: true`). Mapled runs its own checks — the site reads content, the webhook delivered, the preview route responds, fields have help texts, MAPLED.md was written — and the run is completed only when they pass. Fix what failed and call `verify_setup`. The result lands in the file's «Last setup run» section, so call `get_mapled_md` once more when the run is settled and commit the refreshed file.
|
|
52
54
|
|
|
@@ -70,7 +72,7 @@ A rename or a conversion Mapled wouldn't take is refused at once with the reason
|
|
|
70
72
|
| `get_connection` | The delivery key and the API URL, plus — for the `framework` the agent names (`nextjs`, `react-spa`, `plain-html`, …) or the project's own — the package the site reads through (`@mapled/next`, `@mapled/react`, `@mapled/vanilla` — a script tag and `data-mapled-*` attributes for plain HTML — or `@mapled/client`), the env var of the key, whether the site renders on a server or in the browser, and the wire-up steps. Image values → `assetUrl(id, { width })` of the same package |
|
|
71
73
|
| `set_site_url` | Where the site is deployed — for a site rendered in the browser (a React single-page app, plain HTML), which has no webhook to learn it from: Preview opens the site there, verification checks that it answers |
|
|
72
74
|
| `push_site_manifest` / `list_bindings` | Tell Mapled where each field is rendered; read every binding's health (type mismatch, outdated, missing on site). Keep the repository's copy in `mapled/manifest.json` — `npx @mapled/cli scan --write` derives it from the code and `bindings push` / `bindings pull` exchange it with Mapled. A push records the integration hash — the schema and these bindings — and `list_bindings` says whether it still matches (`integration.inSync`, with `schemaChanged` / `bindingsChanged` naming what moved). `capabilities` says whether open tabs follow Publish: `{ realtime: true, releaseRoute: "/api/mapled/release" }` for a Next.js site with `<MapledLive />` and `createReleaseHandler` (on the Pages Router, `createPagesReleaseHandler`), `{ realtime: true }` for a site rendered in the browser whose tabs ask Mapled directly (`live` on `MapledProvider`, `data-mapled-live` on the script tag), `{ realtime: false }` without live updates — only these two keys; MAPLED.md's Live updates line comes from it, so send it with every push |
|
|
73
|
-
| `configure_revalidation` | Point the publish webhook at the site (one with a server that caches what it reads)
|
|
75
|
+
| `configure_revalidation` | Point the publish webhook at the site (one with a server that caches what it reads). No answer carries the signing secret: it names the variable, `MAPLED_WEBHOOK_SECRET`, and the person takes the value from Mapled → Integrations → Your site, where Rotate secret shows a new one once, into the site's env. The description also tells the agent how to add live updates for tabs that are already open (`createReleaseHandler` and `<MapledLive />`, `@mapled/next` 0.8.0+; on the Pages Router `createPagesReleaseHandler` as an API route's default export and `<MapledLive />` from `@mapled/next/live/pages`, 0.9.0+), declared then in the manifest's `capabilities` |
|
|
74
76
|
| `check_integration` | The site's integration as Mapled sees it — delivery reads, the webhook and its last delivery, the bindings summary, the integration hash (is the last push still in step with the schema and the bindings?), current package versions; `npx @mapled/cli doctor` shows the same from inside the repository |
|
|
75
77
|
| `delete_collection` / `remove_field` / `clear_records` / `rename_field` / `convert_field` / `get_confirmation` | Destructive and breaking changes — each files a request a person confirms on a trusted Mapled screen (see above); `get_confirmation` reads its status |
|
|
76
78
|
| `get_mapled_md` | `MAPLED.md` rendered from the project — the guide for the next agent: project, how the site reads it, content model, bindings by page, last setup run, working rules, verification commands. Never a secret. `npx @mapled/cli md pull` writes the same file; `mapled doctor` says when it is out of date |
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { createApiClient,
|
|
4
|
+
import { createApiClient, registerTools } from "./tools.js";
|
|
5
5
|
/** Mapled MCP server. Auth and scope come from the environment:
|
|
6
6
|
MAPLED_MCP_TOKEN — the project token from Connect with AI (required)
|
|
7
7
|
MAPLED_API_URL — API origin (default https://api.mapled.io) */
|
|
@@ -12,27 +12,8 @@ if (!token) {
|
|
|
12
12
|
}
|
|
13
13
|
const baseUrl = process.env.MAPLED_API_URL ?? "https://api.mapled.io";
|
|
14
14
|
const server = new McpServer({ name: "mapled", version: "0.1.0" });
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// runtime contract is identical, so erase the generics here.
|
|
19
|
-
server.tool(tool.name, tool.description, tool.schema, async (args) => {
|
|
20
|
-
try {
|
|
21
|
-
const result = await tool.handler(args);
|
|
22
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
return {
|
|
26
|
-
isError: true,
|
|
27
|
-
content: [
|
|
28
|
-
{
|
|
29
|
-
type: "text",
|
|
30
|
-
text: err instanceof Error ? err.message : "Mapled request failed.",
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
15
|
+
// every call answers through runTool: an error reaches the model only
|
|
16
|
+
// as a text without a Mapled key or token in it (§36.12)
|
|
17
|
+
registerTools(server, createApiClient(baseUrl, token));
|
|
37
18
|
const transport = new StdioServerTransport();
|
|
38
19
|
await server.connect(transport);
|
package/dist/tools.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { z } from "zod";
|
|
|
4
4
|
export type ApiClient = {
|
|
5
5
|
request: (method: "GET" | "POST" | "PATCH", path: string, body?: unknown) => Promise<unknown>;
|
|
6
6
|
};
|
|
7
|
+
/** What a call answers when its request never reached Mapled. */
|
|
8
|
+
export declare const UNREACHABLE = "The request didn't reach Mapled. Try again, or tell the person if it keeps failing: MAPLED_API_URL or MAPLED_MCP_TOKEN in the MCP settings may need a fresh copy. Don't ask them to paste the token into the conversation.";
|
|
7
9
|
export declare function createApiClient(baseUrl: string, token: string): ApiClient;
|
|
8
10
|
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", "computed"];
|
|
9
11
|
export type ToolDef = {
|
|
@@ -12,4 +14,26 @@ export type ToolDef = {
|
|
|
12
14
|
schema: z.ZodRawShape;
|
|
13
15
|
handler: (args: never) => Promise<unknown>;
|
|
14
16
|
};
|
|
17
|
+
/** What a failed call answers when its own text can't pass. */
|
|
18
|
+
export declare const HELD_BACK = "Mapled couldn't complete this call, and its error can't be shown here. Try again, or tell the person if it keeps failing.";
|
|
19
|
+
/** The text an error reaches the model with (§36.12): its message when
|
|
20
|
+
routeText passes it — a route's refusal, as worded — or HELD_BACK.
|
|
21
|
+
`args` are the call's arguments, the runs the model sent. Never
|
|
22
|
+
throws: what escapes a tool's callback the SDK answers with, unread. */
|
|
23
|
+
export declare function errorText(err: unknown, args?: unknown): string;
|
|
24
|
+
export type ToolAnswer = {
|
|
25
|
+
content: {
|
|
26
|
+
type: "text";
|
|
27
|
+
text: string;
|
|
28
|
+
}[];
|
|
29
|
+
isError?: true;
|
|
30
|
+
};
|
|
31
|
+
/** One call as the model reads it, over stdio and the hosted /mcp alike:
|
|
32
|
+
the tool's answer as JSON, or an error that errorText lets through. */
|
|
33
|
+
export declare function runTool(tool: ToolDef, args: unknown): Promise<ToolAnswer>;
|
|
34
|
+
/** The one way a transport offers the tools: every call answers through
|
|
35
|
+
runTool, so no error text reaches the model past it. */
|
|
36
|
+
export declare function registerTools(server: {
|
|
37
|
+
tool: (...args: never[]) => unknown;
|
|
38
|
+
}, api: ApiClient): void;
|
|
15
39
|
export declare function createTools(api: ApiClient): ToolDef[];
|
package/dist/tools.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** What a call answers when its request never reached Mapled. */
|
|
3
|
+
export const UNREACHABLE = "The request didn't reach Mapled. Try again, or tell the person if it keeps failing: MAPLED_API_URL or MAPLED_MCP_TOKEN in the MCP settings may need a fresh copy. Don't ask them to paste the token into the conversation.";
|
|
2
4
|
export function createApiClient(baseUrl, token) {
|
|
3
5
|
return {
|
|
4
6
|
async request(method, path, body) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
let res;
|
|
8
|
+
try {
|
|
9
|
+
res = await fetch(`${baseUrl}${path}`, {
|
|
10
|
+
method,
|
|
11
|
+
headers: {
|
|
12
|
+
authorization: `Bearer ${token}`,
|
|
13
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
14
|
+
},
|
|
15
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// the runtime's text is no route's refusal, and it may quote the
|
|
20
|
+
// request: a header it refused names the token, in pieces too short
|
|
21
|
+
// for any text filter when line breaks cut it (§36.12)
|
|
22
|
+
throw new Error(UNREACHABLE);
|
|
23
|
+
}
|
|
13
24
|
const payload = (await res.json().catch(() => null));
|
|
14
25
|
if (!res.ok) {
|
|
15
26
|
throw new Error(payload?.error?.message ?? `Mapled API error (${res.status}).`);
|
|
@@ -45,6 +56,148 @@ const COMPUTED_HELP = "For type computed only: { expression } — a formula over
|
|
|
45
56
|
"Numbers: + - * / %, round(x, digits), floor, ceil, abs, min, max, sum, avg, count, fixed(x, digits) → text. Text: & joins (empty counts as \"\"), concat, upper, lower, trim, length, left(s, n), right(s, n), replace(s, from, to), contains(s, part), slug(s), text(x), number(s). " +
|
|
46
57
|
"Logic: = != < <= > >=, and, or, not, if(cond, a, b), coalesce(a, b), empty(x). Dates: year, month, day, date(datetime), daysBetween(a, b), addDays(d, n), created() — when the record was added (a datetime). Literals: 12, 2.5, \"text\", true, false, null. " +
|
|
47
58
|
"Sensitive fields and relations, groups, JSON and location can't be read; there is no now(). The result type (number, text, boolean, date, datetime) follows from the formula.";
|
|
59
|
+
/** The variable the site's revalidate route verifies publishes with. */
|
|
60
|
+
const WEBHOOK_SECRET_ENV = "MAPLED_WEBHOOK_SECRET";
|
|
61
|
+
/** The header a publish webhook carries its signature in. */
|
|
62
|
+
const SIGNATURE_HEADER = "x-mapled-signature";
|
|
63
|
+
/** The prefix of every key and token Mapled issues — the report mask's
|
|
64
|
+
`KEYS` (apps/api/src/lib/reportRedaction.ts); the API's
|
|
65
|
+
`security/mcpErrors.test.ts` sends one of each kind through both
|
|
66
|
+
transports. */
|
|
67
|
+
const CREDENTIAL_PREFIX = /(?:whsec|mk_live|msk_live|mcp_live|mcp_oauth|mcp_refresh|mcp_code|mpv|mps)_/i;
|
|
68
|
+
/** What a reader takes a text for: compatibility forms read as ASCII
|
|
69
|
+
(fullwidth letters, the fullwidth low line), and marks, control
|
|
70
|
+
characters other than line breaks and tabs, and characters drawn as
|
|
71
|
+
nothing are gone — a key interleaved with them reads whole. */
|
|
72
|
+
const INVISIBLE = /[\p{M}\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cn}\p{Cs}\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
73
|
+
const readAs = (text) => text.normalize("NFKD").replace(INVISIBLE, "");
|
|
74
|
+
/** Runs of a text that read as a key's or token's random part, each with
|
|
75
|
+
what makes it one:
|
|
76
|
+
- 24 letters and digits — a hex key or secret is 32 or 48;
|
|
77
|
+
- 24 or more characters of base64 or base64url that mix upper- and
|
|
78
|
+
lower-case letters, as a random one does — a key, a slug or a path
|
|
79
|
+
Mapled writes is lower-case, a constant's name upper-case;
|
|
80
|
+
- as many characters of base64url as a token's random part has, 43
|
|
81
|
+
(`generateToken`: 32 bytes), whatever its letters — a random one
|
|
82
|
+
can come out all in one case or with no letter at all. */
|
|
83
|
+
const RANDOM_RUNS = [
|
|
84
|
+
[/[A-Za-z0-9]{24,}/g, () => true],
|
|
85
|
+
[/[A-Za-z0-9+/=_-]{24,}/g, (run) => /[A-Z]/.test(run) && /[a-z]/.test(run)],
|
|
86
|
+
[/[A-Za-z0-9_-]{43,}/g, () => true],
|
|
87
|
+
];
|
|
88
|
+
/** A text as the route words it, or nothing. It never passes when, read as
|
|
89
|
+
a reader would, it holds a Mapled key or token prefix in any letter
|
|
90
|
+
case or a run that reads as a random part (RANDOM_RUNS) the model
|
|
91
|
+
didn't send itself — or when it is longer than 1000 characters. A run
|
|
92
|
+
the model sent (`sent` — the call's arguments), echoed back, tells it
|
|
93
|
+
nothing: the slug it asked for that another record already has passes,
|
|
94
|
+
however long. So a credential whole, with its prefix or its random
|
|
95
|
+
part alone can't reach the model. A text filter can't promise more —
|
|
96
|
+
one split into short pieces or re-encoded passes — and the route's
|
|
97
|
+
refusals have to pass as worded. */
|
|
98
|
+
const routeText = (value, sent = "") => {
|
|
99
|
+
if (typeof value !== "string" || value.length > 1000)
|
|
100
|
+
return undefined;
|
|
101
|
+
const read = readAs(value);
|
|
102
|
+
if (CREDENTIAL_PREFIX.test(read))
|
|
103
|
+
return undefined;
|
|
104
|
+
const random = RANDOM_RUNS.flatMap(([runs, like]) => (read.match(runs) ?? []).filter((run) => like(run)));
|
|
105
|
+
if (random.length === 0)
|
|
106
|
+
return value;
|
|
107
|
+
const known = readAs(sent);
|
|
108
|
+
return random.every((run) => known.includes(run)) ? value : undefined;
|
|
109
|
+
};
|
|
110
|
+
/** What a failed call answers when its own text can't pass. */
|
|
111
|
+
export const HELD_BACK = "Mapled couldn't complete this call, and its error can't be shown here. Try again, or tell the person if it keeps failing.";
|
|
112
|
+
/** The text an error reaches the model with (§36.12): its message when
|
|
113
|
+
routeText passes it — a route's refusal, as worded — or HELD_BACK.
|
|
114
|
+
`args` are the call's arguments, the runs the model sent. Never
|
|
115
|
+
throws: what escapes a tool's callback the SDK answers with, unread. */
|
|
116
|
+
export function errorText(err, args) {
|
|
117
|
+
try {
|
|
118
|
+
return routeText(err instanceof Error ? err.message : undefined, JSON.stringify(args) ?? "") ?? HELD_BACK;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return HELD_BACK;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** One call as the model reads it, over stdio and the hosted /mcp alike:
|
|
125
|
+
the tool's answer as JSON, or an error that errorText lets through. */
|
|
126
|
+
export async function runTool(tool, args) {
|
|
127
|
+
try {
|
|
128
|
+
const result = await tool.handler(args);
|
|
129
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return { isError: true, content: [{ type: "text", text: errorText(err, args) }] };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** The one way a transport offers the tools: every call answers through
|
|
136
|
+
runTool, so no error text reaches the model past it. */
|
|
137
|
+
export function registerTools(server, api) {
|
|
138
|
+
for (const tool of createTools(api)) {
|
|
139
|
+
// The SDK's generic inference recurses on our union of shapes; the
|
|
140
|
+
// runtime contract is identical, so erase the generics here.
|
|
141
|
+
server.tool(tool.name, tool.description, tool.schema, (args) => runTool(tool, args));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** The address the route saved: the agent's own, as the URL parser writes it. */
|
|
145
|
+
const savedUrl = (sent, answered) => {
|
|
146
|
+
try {
|
|
147
|
+
return typeof answered === "string" && answered === new URL(sent).href ? answered : undefined;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/** configure_revalidation: the signing secret never passes through the
|
|
154
|
+
agent (§36.12) — the answer names the variable and hands the person
|
|
155
|
+
the step, over stdio and the hosted endpoint alike. Only the route's
|
|
156
|
+
own shapes pass: the agent's address as saved, the signature header's
|
|
157
|
+
name, a status of its two words, and texts — a refusal's too — that
|
|
158
|
+
carry no secret whole or with its prefix. */
|
|
159
|
+
function configureRevalidation(api) {
|
|
160
|
+
return {
|
|
161
|
+
name: "configure_revalidation",
|
|
162
|
+
description: "Point Mapled's publish webhook at the site so published changes appear instantly. Only for a site with " +
|
|
163
|
+
"a server that caches what it reads (Next.js and the like); a site rendered in the browser (react-spa, " +
|
|
164
|
+
"plain-html) shows a publish on the next load and needs none — call set_site_url for it instead. " +
|
|
165
|
+
"Pass the site's public revalidate URL (with @mapled/next: mount createRevalidateHandler " +
|
|
166
|
+
"from \"@mapled/next/server\" at /api/mapled/revalidate and pass that URL here). " +
|
|
167
|
+
"The webhook's signing secret never passes through you: the answer says how the person sets " +
|
|
168
|
+
`${WEBHOOK_SECRET_ENV} in the site's env — they copy it from Mapled → Integrations → Your site. ` +
|
|
169
|
+
"Don't ask them to paste it into the conversation. " +
|
|
170
|
+
"Local and private URLs are rejected; use the deployed site's URL. " +
|
|
171
|
+
"Optional, when the owner wants pages that are already open to follow a publish without a reload " +
|
|
172
|
+
"(@mapled/next 0.8.0+): also mount createReleaseHandler({ key: process.env.MAPLED_KEY! }) from " +
|
|
173
|
+
"\"@mapled/next/server\" as GET at /api/mapled/release and render <MapledLive /> from " +
|
|
174
|
+
"\"@mapled/next/live\" once in the root layout. On the Pages Router (@mapled/next 0.9.0+) export " +
|
|
175
|
+
"createPagesReleaseHandler({ key: process.env.MAPLED_KEY! }) from \"@mapled/next/server\" as the default " +
|
|
176
|
+
"of pages/api/mapled/release.ts and render <MapledLive /> from \"@mapled/next/live/pages\" in pages/_app; " +
|
|
177
|
+
"there only pages that read in getServerSideProps follow a publish (the @mapled/next README, Live updates → " +
|
|
178
|
+
"Pages Router). Tabs poll the site's own route, never Mapled; then declare it in push_site_manifest's " +
|
|
179
|
+
"capabilities.",
|
|
180
|
+
schema: {
|
|
181
|
+
url: z.string().min(8).max(2048),
|
|
182
|
+
},
|
|
183
|
+
handler: async (args) => {
|
|
184
|
+
let hook;
|
|
185
|
+
try {
|
|
186
|
+
hook = ((await api.request("PATCH", "/v1/agent/webhook", { url: args.url })) ?? {});
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
throw new Error(routeText(err instanceof Error ? err.message : undefined, JSON.stringify(args)) ?? "Mapled couldn't save the webhook. Try again.");
|
|
190
|
+
}
|
|
191
|
+
const status = hook.signingSecret?.status;
|
|
192
|
+
return {
|
|
193
|
+
url: savedUrl(args.url, hook.url),
|
|
194
|
+
signatureHeader: hook.signatureHeader === SIGNATURE_HEADER ? SIGNATURE_HEADER : undefined,
|
|
195
|
+
signingSecret: { env: WEBHOOK_SECRET_ENV, status: status === "created" || status === "existing" ? status : undefined },
|
|
196
|
+
next: routeText(hook.next),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
48
201
|
export function createTools(api) {
|
|
49
202
|
return [
|
|
50
203
|
{
|
|
@@ -547,29 +700,7 @@ export function createTools(api) {
|
|
|
547
700
|
},
|
|
548
701
|
handler: async (args) => api.request("PATCH", "/v1/agent/site", { url: args.url }),
|
|
549
702
|
},
|
|
550
|
-
|
|
551
|
-
name: "configure_revalidation",
|
|
552
|
-
description: "Point Mapled's publish webhook at the site so published changes appear instantly. Only for a site with " +
|
|
553
|
-
"a server that caches what it reads (Next.js and the like); a site rendered in the browser (react-spa, " +
|
|
554
|
-
"plain-html) shows a publish on the next load and needs none — call set_site_url for it instead. " +
|
|
555
|
-
"Pass the site's public revalidate URL (with @mapled/next: mount createRevalidateHandler " +
|
|
556
|
-
"from \"@mapled/next/server\" at /api/mapled/revalidate and pass that URL here). " +
|
|
557
|
-
"Returns the signing secret — store it in the site's env as MAPLED_WEBHOOK_SECRET. " +
|
|
558
|
-
"Local and private URLs are rejected; use the deployed site's URL. " +
|
|
559
|
-
"Optional, when the owner wants pages that are already open to follow a publish without a reload " +
|
|
560
|
-
"(@mapled/next 0.8.0+): also mount createReleaseHandler({ key: process.env.MAPLED_KEY! }) from " +
|
|
561
|
-
"\"@mapled/next/server\" as GET at /api/mapled/release and render <MapledLive /> from " +
|
|
562
|
-
"\"@mapled/next/live\" once in the root layout. On the Pages Router (@mapled/next 0.9.0+) export " +
|
|
563
|
-
"createPagesReleaseHandler({ key: process.env.MAPLED_KEY! }) from \"@mapled/next/server\" as the default " +
|
|
564
|
-
"of pages/api/mapled/release.ts and render <MapledLive /> from \"@mapled/next/live/pages\" in pages/_app; " +
|
|
565
|
-
"there only pages that read in getServerSideProps follow a publish (the @mapled/next README, Live updates → " +
|
|
566
|
-
"Pages Router). Tabs poll the site's own route, never Mapled; then declare it in push_site_manifest's " +
|
|
567
|
-
"capabilities.",
|
|
568
|
-
schema: {
|
|
569
|
-
url: z.string().min(8).max(2048),
|
|
570
|
-
},
|
|
571
|
-
handler: async (args) => api.request("PATCH", "/v1/agent/webhook", { url: args.url }),
|
|
572
|
-
},
|
|
703
|
+
configureRevalidation(api),
|
|
573
704
|
{
|
|
574
705
|
name: "get_mapled_md",
|
|
575
706
|
description: "Get MAPLED.md — the guide Mapled writes for the next agent and for people: the project and how the site " +
|