@mediamonks/canopy-cli 0.7.31 → 0.7.33
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/dist/{deploy-B-znPE-v.mjs → deploy-Ka2lHkyy.mjs} +111 -1
- package/dist/docs/agent-tools.md +4 -5
- package/dist/docs/cli-create.md +1 -0
- package/dist/docs/cli-repo.md +87 -0
- package/dist/docs/getting-started.md +3 -0
- package/dist/docs/git.md +86 -0
- package/dist/docs/index.json +15 -0
- package/dist/docs/sdk-mcp.md +222 -0
- package/dist/{docs-YD4z70gq.mjs → docs-BP5jmD10.mjs} +1 -1
- package/dist/{feedback-Cd7Rvg9a.mjs → feedback-5oPSxAz0.mjs} +1 -1
- package/dist/index.mjs +19 -18
- package/dist/{login-B-m-i8uu.mjs → login-D0J5s_Ex.mjs} +3 -1
- package/dist/{mcp-GKwctfHN.mjs → mcp-DXfTYVN_.mjs} +1 -1
- package/dist/{package-CiKTWo0_.mjs → package-CQIPmdcO.mjs} +1 -1
- package/dist/{projects-CN2YhCZn.mjs → projects-DQAhCZm8.mjs} +13 -1
- package/dist/repo-1RQ1vBMv.mjs +2 -0
- package/dist/repo-Bb9ntNa4.mjs +423 -0
- package/dist/repo-credential-CNDRM-tG.mjs +118 -0
- package/dist/skill/canopy/SKILL.md +7 -1
- package/dist/{skill-BWmXiJAz.mjs → skill-Dg7dQF0k.mjs} +1 -1
- package/dist/templates/spa/AGENTS.md +13 -0
- package/dist/templates/spa/_gitignore +4 -0
- package/dist/templates/static/AGENTS.md +11 -0
- package/dist/templates/static/_gitignore +4 -0
- package/dist/templates/worker/AGENTS.md +13 -0
- package/dist/templates/worker/_gitignore +3 -0
- package/dist/{upgrade-Cexs9KYX.mjs → upgrade-huJDjhrV.mjs} +1 -1
- package/package.json +2 -1
|
@@ -2,6 +2,7 @@ import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteCon
|
|
|
2
2
|
import { i as apiRequest, n as apiDataRequest, o as withContext, r as apiDownload } from "./api-C4rMhE7i.mjs";
|
|
3
3
|
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|
|
4
4
|
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Czlcddyz.mjs";
|
|
5
|
+
import { builtinModules } from "node:module";
|
|
5
6
|
import { defineCommand } from "citty";
|
|
6
7
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
@@ -27,7 +28,7 @@ async function bundleWorker(projectDir, entry) {
|
|
|
27
28
|
format: "esm",
|
|
28
29
|
target: "es2022",
|
|
29
30
|
packages: "bundle",
|
|
30
|
-
|
|
31
|
+
plugins: [nodeBuiltins],
|
|
31
32
|
logLevel: "silent"
|
|
32
33
|
});
|
|
33
34
|
return new Uint8Array(readFileSync(outfile));
|
|
@@ -38,6 +39,115 @@ async function bundleWorker(projectDir, entry) {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/** Bare names Node resolves to a built-in (`fs`, `fs/promises`, …). */
|
|
43
|
+
const BARE_BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
|
|
44
|
+
/**
|
|
45
|
+
* Built-ins both hosts provide, each with a default export: Node, and workerd
|
|
46
|
+
* at the dispatch scripts' compatibility date (2026-05-01, nodejs_compat —
|
|
47
|
+
* probed with a worker importing every one; several are stubs that throw only
|
|
48
|
+
* when called). Missing on workerd: `sea`, `test/reporters`. Left out on
|
|
49
|
+
* purpose: `sqlite` (experimental on Node) and `test`.
|
|
50
|
+
*/
|
|
51
|
+
const HOSTED_BUILTINS = /* @__PURE__ */ new Set([
|
|
52
|
+
"assert",
|
|
53
|
+
"assert/strict",
|
|
54
|
+
"async_hooks",
|
|
55
|
+
"buffer",
|
|
56
|
+
"child_process",
|
|
57
|
+
"cluster",
|
|
58
|
+
"console",
|
|
59
|
+
"constants",
|
|
60
|
+
"crypto",
|
|
61
|
+
"dgram",
|
|
62
|
+
"diagnostics_channel",
|
|
63
|
+
"dns",
|
|
64
|
+
"dns/promises",
|
|
65
|
+
"domain",
|
|
66
|
+
"events",
|
|
67
|
+
"fs",
|
|
68
|
+
"fs/promises",
|
|
69
|
+
"http",
|
|
70
|
+
"http2",
|
|
71
|
+
"https",
|
|
72
|
+
"inspector",
|
|
73
|
+
"inspector/promises",
|
|
74
|
+
"module",
|
|
75
|
+
"net",
|
|
76
|
+
"os",
|
|
77
|
+
"path",
|
|
78
|
+
"path/posix",
|
|
79
|
+
"path/win32",
|
|
80
|
+
"perf_hooks",
|
|
81
|
+
"process",
|
|
82
|
+
"punycode",
|
|
83
|
+
"querystring",
|
|
84
|
+
"readline",
|
|
85
|
+
"readline/promises",
|
|
86
|
+
"repl",
|
|
87
|
+
"stream",
|
|
88
|
+
"stream/consumers",
|
|
89
|
+
"stream/promises",
|
|
90
|
+
"stream/web",
|
|
91
|
+
"string_decoder",
|
|
92
|
+
"sys",
|
|
93
|
+
"timers",
|
|
94
|
+
"timers/promises",
|
|
95
|
+
"tls",
|
|
96
|
+
"trace_events",
|
|
97
|
+
"tty",
|
|
98
|
+
"url",
|
|
99
|
+
"util",
|
|
100
|
+
"util/types",
|
|
101
|
+
"v8",
|
|
102
|
+
"vm",
|
|
103
|
+
"wasi",
|
|
104
|
+
"worker_threads",
|
|
105
|
+
"zlib"
|
|
106
|
+
]);
|
|
107
|
+
const REQUIRE_NAMESPACE = "canopy-node-require";
|
|
108
|
+
/**
|
|
109
|
+
* Node built-ins are the host's, not the bundle's: the GCP runtime is Node and
|
|
110
|
+
* Cloudflare dispatch scripts run with nodejs_compat. A `neutral` build cannot
|
|
111
|
+
* resolve them at all, so every import of one — `node:fs` or bare `fs`, as Node
|
|
112
|
+
* itself treats them — stays external under its `node:` name.
|
|
113
|
+
*
|
|
114
|
+
* A CommonJS `require()` of one cannot simply stay external: in an ESM bundle
|
|
115
|
+
* esbuild turns it into a `__require` shim that throws "Dynamic require of …
|
|
116
|
+
* is not supported" when it runs. Neither host offers a `require` to an ES
|
|
117
|
+
* module, so the call is routed to a tiny ES module that re-exports the
|
|
118
|
+
* built-in through a static `import`. The AI SDK needs this: `ai` reaches
|
|
119
|
+
* `@vercel/oidc`, whose CommonJS build requires `fs`, `os` and `path` at load.
|
|
120
|
+
*
|
|
121
|
+
* That turns a lazy require — often an optional one in a try/catch — into an
|
|
122
|
+
* import made at load, and Cloudflare refuses a script importing a module it
|
|
123
|
+
* lacks. So only HOSTED_BUILTINS are rewritten; a require of any other
|
|
124
|
+
* built-in keeps the throwing shim, which fails only if that line runs.
|
|
125
|
+
*/
|
|
126
|
+
const nodeBuiltins = {
|
|
127
|
+
name: "canopy-node-builtins",
|
|
128
|
+
setup(build) {
|
|
129
|
+
build.onResolve({ filter: /^(node:)?[a-z_][a-z0-9_/]*$/ }, (args) => {
|
|
130
|
+
const prefixed = args.path.startsWith("node:");
|
|
131
|
+
const name = prefixed ? args.path.slice(5) : args.path;
|
|
132
|
+
if (!prefixed && !BARE_BUILTINS.has(name)) return void 0;
|
|
133
|
+
if (args.kind === "require-call" && HOSTED_BUILTINS.has(name)) return {
|
|
134
|
+
path: name,
|
|
135
|
+
namespace: REQUIRE_NAMESPACE
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
path: `node:${name}`,
|
|
139
|
+
external: true
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
build.onLoad({
|
|
143
|
+
filter: /.*/,
|
|
144
|
+
namespace: REQUIRE_NAMESPACE
|
|
145
|
+
}, (args) => ({
|
|
146
|
+
contents: `export * from "node:${args.path}";\nexport { default } from "node:${args.path}";\n`,
|
|
147
|
+
loader: "js"
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
41
151
|
//#endregion
|
|
42
152
|
//#region src/deploy.ts
|
|
43
153
|
/**
|
package/dist/docs/agent-tools.md
CHANGED
|
@@ -101,7 +101,7 @@ wrote for another MCP server carry over as they are.
|
|
|
101
101
|
A tool set you already use with the AI SDK works too. Put it in the same list:
|
|
102
102
|
|
|
103
103
|
```ts
|
|
104
|
-
import { tool } from "
|
|
104
|
+
import { tool } from "ai";
|
|
105
105
|
|
|
106
106
|
const aiTools = {
|
|
107
107
|
weather: tool({
|
|
@@ -114,9 +114,7 @@ const aiTools = {
|
|
|
114
114
|
mountMcpTools(app, [...myTools, aiTools]);
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
package doesn't bundle for Canopy backends yet. Tools without `execute` (ones
|
|
119
|
-
the AI provider runs itself) are left out.
|
|
117
|
+
Tools without `execute` (ones the AI provider runs itself) are left out.
|
|
120
118
|
|
|
121
119
|
## Write good tools
|
|
122
120
|
|
|
@@ -210,4 +208,5 @@ an agent (`agent`), in any route. Two limits apply to your own server:
|
|
|
210
208
|
60 seconds.
|
|
211
209
|
- Agent calls show up in your backend's logs like any other request.
|
|
212
210
|
|
|
213
|
-
See [
|
|
211
|
+
See the [SDK reference](https://docs.canopy.website/sdk/mcp) for every option and [canopy mcp](https://docs.canopy.website/cli/mcp)
|
|
212
|
+
for every flag.
|
package/dist/docs/cli-create.md
CHANGED
|
@@ -19,6 +19,7 @@ canopy create existing-app --no-scaffold # register only, no files written
|
|
|
19
19
|
- `--slug <slug>` — the URL slug; the project will live at `https://<slug>.<site-domain>`. Lowercase letters, digits and dashes only. Defaults to a slugified version of the name (prompted on a TTY).
|
|
20
20
|
- `--template <id>` — starter to scaffold: `static` (plain HTML/CSS/JS, no build step, untyped SDK from the platform — simplest apps), `spa` (Vite + React + TypeScript with the typed SDK from npm — best for real SDK use or growth), or `worker` (`spa` plus a backend worker in `worker/` — for server-side code: secrets, schedules, calling external APIs). A picker appears when omitted on a TTY; `static` is the default otherwise.
|
|
21
21
|
- `--scaffold` / `--no-scaffold` — whether to write `./<slug>/` from the template. When neither is given, the CLI scaffolds unless the current directory already has a `canopy.json` (in which case it assumes you're registering an existing app and only registers).
|
|
22
|
+
- `--repo` / `--no-repo` — also create a git repository for the project on Canopy and set up the new folder to push to it (see [`canopy repo`](https://docs.canopy.website/cli/repo)). When neither is given, an interactive run asks (default no) on sites that offer repositories; non-interactive runs create none.
|
|
22
23
|
- `--profile <site>` — global flag (or `CANOPY_PROFILE`): which site to create the project on. The chosen profile is pinned into the scaffolded `canopy.json`.
|
|
23
24
|
|
|
24
25
|
## How it works
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy repo
|
|
3
|
+
description: Create, clone and set up the project's git repository on Canopy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy repo` manages the project's git repository. A project has at most one: a
|
|
7
|
+
Canopy repository, which Canopy keeps for it, or its own GitHub repository.
|
|
8
|
+
Once a folder is set up, you use plain git. The CLI signs git in on every push
|
|
9
|
+
and pull, so you never handle a password or token.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
canopy repo show [--json]
|
|
15
|
+
canopy repo create
|
|
16
|
+
canopy repo clone <slug> [folder]
|
|
17
|
+
canopy repo connect
|
|
18
|
+
canopy repo delete [--yes]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Arguments & flags
|
|
22
|
+
|
|
23
|
+
`show`, `create`, `connect` and `delete` resolve the project the usual way:
|
|
24
|
+
`canopy.json` in the target folder, or `--project <slug>`. All of them honour
|
|
25
|
+
the global `--profile <site>` / `CANOPY_PROFILE`.
|
|
26
|
+
|
|
27
|
+
- `show` — whether the project has a repository, and if so its remote, when it
|
|
28
|
+
was created and when it was last pushed to.
|
|
29
|
+
- `--json` — the same as JSON, for scripts and agents.
|
|
30
|
+
- `create` — create the project's Canopy repository. Owners, editors and
|
|
31
|
+
platform admins can. Run it in the project's folder and the folder is set up
|
|
32
|
+
too.
|
|
33
|
+
- `clone <slug> [folder]` — clone the project's repository into a new folder
|
|
34
|
+
(the slug by default), ready to push.
|
|
35
|
+
- `connect` — set up an existing folder.
|
|
36
|
+
- If the folder isn't a git repository yet, it is initialised on branch `main`.
|
|
37
|
+
- If it has no `origin`, the Canopy repository becomes `origin`.
|
|
38
|
+
- If `origin` points somewhere else, the Canopy repository is added as
|
|
39
|
+
`canopy`.
|
|
40
|
+
- `delete` — delete the repository and all its history. Owner only. Asks you to
|
|
41
|
+
type the slug.
|
|
42
|
+
- `--yes` — skip that question.
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
Setting up a folder (`create`, `clone`, `connect`) does two things:
|
|
47
|
+
|
|
48
|
+
1. It points a remote at the project's repository.
|
|
49
|
+
2. It registers the CLI as git's credential helper for that one repository.
|
|
50
|
+
|
|
51
|
+
Nothing secret is written into the folder or its git settings. On each push or
|
|
52
|
+
pull, git asks the CLI for credentials. The CLI exchanges your `canopy login`
|
|
53
|
+
for a short-lived token that only works for this repository, keeps it in
|
|
54
|
+
`~/.canopy` for up to an hour, and gets a new one when needed. `canopy logout`
|
|
55
|
+
throws them away.
|
|
56
|
+
|
|
57
|
+
Pushing doesn't deploy. Run `canopy deploy` for that.
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
canopy repo create
|
|
63
|
+
git add -A && git commit -m "First version" && git push -u origin main
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Creates the repository for the project in this folder and pushes the code.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
canopy repo clone my-tool
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Clones a teammate's project into `./my-tool`, ready to push.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
canopy repo show --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"kind": "canopy",
|
|
81
|
+
"remote": "https://monks.ghe.com/canopy/my-tool.git",
|
|
82
|
+
"created_at": "2026-10-21T09:12:00Z",
|
|
83
|
+
"last_push_at": "2026-10-21T09:15:42Z"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Read the [git guide](https://docs.canopy.website/guides/git) for the bigger picture.
|
|
@@ -93,6 +93,9 @@ Then open the live site:
|
|
|
93
93
|
canopy open
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
Want your code in git too, without a GitHub account? `canopy repo create` gives
|
|
97
|
+
the project a repository on Canopy — see [Keep your code in git](https://docs.canopy.website/guides/git).
|
|
98
|
+
|
|
96
99
|
## Where to go next
|
|
97
100
|
|
|
98
101
|
Everything the CLI can do is documented and available offline. List the topics:
|
package/dist/docs/git.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Keep your code in git
|
|
3
|
+
description: Give your project a git repository on Canopy — history, backup and sharing, no GitHub account needed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every project can have one git repository. If you already use GitHub, keep using
|
|
7
|
+
it. If you don't have a GitHub account, Canopy can keep a private repository for
|
|
8
|
+
your project instead. Your code then has history and a backup, and teammates and
|
|
9
|
+
their agents can pick it up.
|
|
10
|
+
|
|
11
|
+
A Canopy repository is for storing code. Pushing to it doesn't deploy anything —
|
|
12
|
+
`canopy deploy` still does that.
|
|
13
|
+
|
|
14
|
+
## Create one
|
|
15
|
+
|
|
16
|
+
From your project folder:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
canopy repo create
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That creates the repository and sets up the folder, so from then on it is plain
|
|
23
|
+
git:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git add -A
|
|
27
|
+
git commit -m "First version"
|
|
28
|
+
git push -u origin main
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
New projects can get one straight away with `canopy create my-tool --repo`. You
|
|
32
|
+
can also create one from the dashboard, on the project's **Deploys** tab.
|
|
33
|
+
|
|
34
|
+
## Work on it from another machine (or hand it to a teammate)
|
|
35
|
+
|
|
36
|
+
Anyone on the project can clone it:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
canopy repo clone my-tool
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Already have the code in a folder? Set that folder up instead:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
canopy repo connect
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the folder already pushes somewhere else (say your own GitHub), that stays
|
|
49
|
+
`origin` and the Canopy repository is added as a second remote called `canopy`.
|
|
50
|
+
|
|
51
|
+
## Signing in
|
|
52
|
+
|
|
53
|
+
You never type a password or paste a token. The Canopy CLI signs git in for you
|
|
54
|
+
on every push and pull, using your `canopy login`. When that sign-in expires, git
|
|
55
|
+
stops and tells you to run `canopy login` again.
|
|
56
|
+
|
|
57
|
+
Who can do what follows the project's members:
|
|
58
|
+
|
|
59
|
+
- Owners and editors can pull and push.
|
|
60
|
+
- Platform admins can pull.
|
|
61
|
+
- Nobody else gets in.
|
|
62
|
+
|
|
63
|
+
Remove someone from the project and they can no longer push or pull, even from
|
|
64
|
+
a copy they already cloned.
|
|
65
|
+
|
|
66
|
+
## Git apps (GitHub Desktop, VS Code, Tower, …)
|
|
67
|
+
|
|
68
|
+
Set the folder up once with `canopy repo clone` or `canopy repo connect` in a
|
|
69
|
+
terminal. After that, git apps that use the git installed on your computer push
|
|
70
|
+
and pull as usual. Apps that bring their own git or ask for a password can't
|
|
71
|
+
sign in to a Canopy repository; use the terminal for those.
|
|
72
|
+
|
|
73
|
+
## Good to know
|
|
74
|
+
|
|
75
|
+
- **Keep secrets out.** Files like `.env` and `.dev.vars` are ignored by the
|
|
76
|
+
project templates; secrets belong in `canopy secrets`.
|
|
77
|
+
- **There is no branch protection.** Anyone who can push can rewrite history, so
|
|
78
|
+
avoid force-pushes on `main`.
|
|
79
|
+
- **Deleting is for good.** `canopy repo delete` (owner only) removes the
|
|
80
|
+
repository and all its history. Deleting the project deletes its repository
|
|
81
|
+
too.
|
|
82
|
+
- **Moving to your own GitHub later** is ordinary git: create an empty
|
|
83
|
+
repository there, then `git remote add github <its url>` and
|
|
84
|
+
`git push github --all`.
|
|
85
|
+
|
|
86
|
+
See the [`canopy repo` reference](https://docs.canopy.website/cli/repo) for every command and flag.
|
package/dist/docs/index.json
CHANGED
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
"title": "CI and deploy tokens",
|
|
68
68
|
"summary": "Push-to-main deploys using a project-scoped deploy token."
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"slug": "git",
|
|
72
|
+
"title": "Keep your code in git",
|
|
73
|
+
"summary": "Give your project a git repository on Canopy — history, backup and sharing, no GitHub account needed."
|
|
74
|
+
},
|
|
70
75
|
{
|
|
71
76
|
"slug": "agent-tools",
|
|
72
77
|
"title": "Agent tools (MCP)",
|
|
@@ -251,6 +256,11 @@
|
|
|
251
256
|
"title": "canopy token",
|
|
252
257
|
"summary": "Create, list, and revoke project-scoped deploy tokens for CI and push-to-main deploys."
|
|
253
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"slug": "cli-repo",
|
|
261
|
+
"title": "canopy repo",
|
|
262
|
+
"summary": "Create, clone and set up the project's git repository on Canopy."
|
|
263
|
+
},
|
|
254
264
|
{
|
|
255
265
|
"slug": "cli-egress",
|
|
256
266
|
"title": "canopy egress",
|
|
@@ -363,6 +373,11 @@
|
|
|
363
373
|
"title": "Realtime",
|
|
364
374
|
"summary": "Live updates between the people using your app — subscribe to a topic, publish to it, no backend needed."
|
|
365
375
|
},
|
|
376
|
+
{
|
|
377
|
+
"slug": "sdk-mcp",
|
|
378
|
+
"title": "Agent tools (MCP)",
|
|
379
|
+
"summary": "\"@mediamonks/canopy/mcp — serve your backend's tools to AI agents: mountMcpTools, createMcpToolsHandler, MCP_PATH and mcpCaller.\""
|
|
380
|
+
},
|
|
366
381
|
{
|
|
367
382
|
"slug": "sdk-google",
|
|
368
383
|
"title": "Google",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Agent tools (MCP)
|
|
3
|
+
description: "@mediamonks/canopy/mcp — serve your backend's tools to AI agents: mountMcpTools, createMcpToolsHandler, MCP_PATH and mcpCaller."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`@mediamonks/canopy/mcp` turns your backend into a set of MCP tools that AI
|
|
7
|
+
agents call through `canopy mcp serve`, always as the signed-in Monk running
|
|
8
|
+
them. It is a separate entry of the SDK, for backend code only: browser
|
|
9
|
+
bundles never include it.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createMcpToolsHandler, MCP_PATH, mcpCaller, mountMcpTools } from "@mediamonks/canopy/mcp";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For the why and the how-to, read the [Agent tools guide](https://docs.canopy.website/guides/agent-tools).
|
|
16
|
+
This page lists the exports and what they accept.
|
|
17
|
+
|
|
18
|
+
## mountMcpTools(app, tools, options?)
|
|
19
|
+
|
|
20
|
+
Serves `tools` at `MCP_PATH` on a Hono app. Route tools run this same app's
|
|
21
|
+
routes.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { mountMcpTools } from "@mediamonks/canopy/mcp";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
|
|
27
|
+
mountMcpTools(app, [
|
|
28
|
+
{
|
|
29
|
+
name: "list_notes",
|
|
30
|
+
description: "List the current user's notes, newest first",
|
|
31
|
+
route: "GET /api/notes",
|
|
32
|
+
annotations: { readOnlyHint: true },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "summarise",
|
|
36
|
+
description: "Summarise a note",
|
|
37
|
+
inputSchema: z.object({ id: z.string() }),
|
|
38
|
+
handler: async ({ id }, ctx) => summarise(ctx.env, id),
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `app` — your Hono app, or anything with `all(path, handler)` and
|
|
44
|
+
`fetch(request, env, ctx)`.
|
|
45
|
+
- `tools` — a list of [tool definitions](#tool-definitions) and
|
|
46
|
+
[AI SDK tool sets](#vercel-ai-sdk-tool-sets), mixed.
|
|
47
|
+
- `options` — optional `{ name?, version? }`: the server name and version MCP
|
|
48
|
+
clients see. The name defaults to your project's slug.
|
|
49
|
+
|
|
50
|
+
## createMcpToolsHandler(tools, options?)
|
|
51
|
+
|
|
52
|
+
The same thing without Hono: returns a handler you route `MCP_PATH` to
|
|
53
|
+
yourself.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createMcpToolsHandler, MCP_PATH } from "@mediamonks/canopy/mcp";
|
|
57
|
+
|
|
58
|
+
const tools = createMcpToolsHandler(myTools, { app });
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
fetch(request, env, ctx) {
|
|
62
|
+
if (new URL(request.url).pathname === MCP_PATH) return tools(request, env, ctx);
|
|
63
|
+
return app.fetch(request, env, ctx);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Returns** `(request, env?, executionCtx?) => Promise<Response>`. Pass the
|
|
69
|
+
backend's `env` so handlers and route tools see `env.USER`, `env.CANOPY` and
|
|
70
|
+
your secrets.
|
|
71
|
+
|
|
72
|
+
**Options:** `{ app?, name?, version? }`. `app` is required when the list has
|
|
73
|
+
route tools: they run its routes.
|
|
74
|
+
|
|
75
|
+
Each request gets a fresh, stateless MCP server built on the official
|
|
76
|
+
`@modelcontextprotocol/server`. It speaks both the current MCP protocol and
|
|
77
|
+
the 2025 revision, so older clients work too.
|
|
78
|
+
|
|
79
|
+
## Tool definitions
|
|
80
|
+
|
|
81
|
+
A tool is an object in the MCP specification's shape, plus either a `handler`
|
|
82
|
+
or a `route`:
|
|
83
|
+
|
|
84
|
+
| Field | Type | |
|
|
85
|
+
| -------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
|
|
86
|
+
| `name` | `string` | Required. Unique; letters, digits, `_`, `-` and `.`, at most 128. |
|
|
87
|
+
| `description` | `string` | What it does and when to use it. The agent reads this. |
|
|
88
|
+
| `title` | `string` | A display name. |
|
|
89
|
+
| `inputSchema` | JSON Schema or Standard Schema | The arguments. Defaults to "no arguments". |
|
|
90
|
+
| `outputSchema` | JSON Schema or Standard Schema | Shape of the structured result, if you want clients to rely on it. |
|
|
91
|
+
| `annotations` | `McpToolAnnotations` | Hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title`. |
|
|
92
|
+
| `handler` | `(args, ctx) => unknown` | Runs the tool. |
|
|
93
|
+
| `route` | `"<METHOD> /path"` | Runs one of your routes instead. |
|
|
94
|
+
|
|
95
|
+
**Schemas** can be plain JSON Schema or any schema that implements Standard
|
|
96
|
+
Schema with JSON Schema output: Zod 4, Valibot (with `toStandardJsonSchema`),
|
|
97
|
+
ArkType. Arguments are validated against `inputSchema` before your handler or
|
|
98
|
+
route runs. Invalid arguments come back to the agent as an error naming the
|
|
99
|
+
field.
|
|
100
|
+
|
|
101
|
+
**Annotations** matter to agents. Mark tools that change nothing with
|
|
102
|
+
`readOnlyHint: true`. Unmarked tools are treated as possibly destructive, and
|
|
103
|
+
clients usually ask the user before calling them.
|
|
104
|
+
|
|
105
|
+
### Handler tools
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
{
|
|
109
|
+
name: "add_comment",
|
|
110
|
+
inputSchema: z.object({ noteId: z.string(), text: z.string().max(2000) }),
|
|
111
|
+
handler: async ({ noteId, text }, ctx) => {
|
|
112
|
+
const env = ctx.env as Env; // your backend's Env type
|
|
113
|
+
const canopy = createCanopy({ fetch: env.CANOPY.fetch.bind(env.CANOPY) });
|
|
114
|
+
return canopy.db.collection("comments").insert({ noteId, text, by: ctx.user?.email });
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
What the handler returns becomes the result:
|
|
120
|
+
|
|
121
|
+
- an object — sent as structured content, with a JSON text copy;
|
|
122
|
+
- a string — sent as text;
|
|
123
|
+
- an MCP `CallToolResult` (`{ content, structuredContent?, isError? }`) —
|
|
124
|
+
sent as is;
|
|
125
|
+
- nothing — an empty result.
|
|
126
|
+
|
|
127
|
+
Throw to report a failure. The agent gets a tool error with your message.
|
|
128
|
+
|
|
129
|
+
**`ctx`** (`McpToolContext`):
|
|
130
|
+
|
|
131
|
+
- `user` — the signed-in Monk (`{ sub, email, name }`), or `null`.
|
|
132
|
+
- `agent` — `true` when an AI agent made the call through `canopy mcp`.
|
|
133
|
+
- `toolCallId` — this call's id, for logs.
|
|
134
|
+
- `signal` — an `AbortSignal`, aborted when the agent cancels.
|
|
135
|
+
- `request` — the HTTP request the call arrived on, without platform
|
|
136
|
+
credentials.
|
|
137
|
+
- `env` — your backend's env, as routes get it. Typed `unknown`: cast it to
|
|
138
|
+
your `Env`.
|
|
139
|
+
|
|
140
|
+
`canopy.*` calls from a handler act as the user, as they would for that
|
|
141
|
+
person's browser.
|
|
142
|
+
|
|
143
|
+
### Route tools
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
{ name: "get_note", route: "GET /api/notes/:id", inputSchema: z.object({ id: z.string() }) }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The route runs in-process, with the same `env` and identity:
|
|
150
|
+
|
|
151
|
+
- `:name` segments of the path are filled from the argument of that name. A
|
|
152
|
+
missing one is a tool error.
|
|
153
|
+
- `GET` and `DELETE`: the other arguments become the query string (arrays
|
|
154
|
+
repeat the key, objects are sent as JSON).
|
|
155
|
+
- `POST`, `PUT`, `PATCH`: the other arguments become a JSON body.
|
|
156
|
+
- A 2xx JSON response becomes the result (structured when it's an object).
|
|
157
|
+
Other 2xx bodies become text.
|
|
158
|
+
- Any other status becomes a tool error: `HTTP 403: {"error":"not_owner"}`.
|
|
159
|
+
|
|
160
|
+
### Vercel AI SDK tool sets
|
|
161
|
+
|
|
162
|
+
Put a tool set in the list as it is. Each key becomes a tool name:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { tool } from "ai";
|
|
166
|
+
|
|
167
|
+
const aiTools = {
|
|
168
|
+
weather: tool({
|
|
169
|
+
description: "Weather in a city",
|
|
170
|
+
inputSchema: z.object({ city: z.string() }),
|
|
171
|
+
execute: async ({ city }) => ({ city, tempC: 21 }),
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
mountMcpTools(app, [...myTools, aiTools]);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`execute` gets the arguments and `{ toolCallId, abortSignal, messages: [] }`.
|
|
179
|
+
Tools without `execute` are skipped. If the result is a stream, its last value
|
|
180
|
+
is the answer.
|
|
181
|
+
|
|
182
|
+
## MCP_PATH
|
|
183
|
+
|
|
184
|
+
`"/__canopy/mcp"` — the one path on your backend that agents reach. Mount your
|
|
185
|
+
own MCP server there if you don't use `mountMcpTools`. It must answer on that
|
|
186
|
+
single URL with MCP's streamable HTTP transport.
|
|
187
|
+
|
|
188
|
+
## mcpCaller(request, env?)
|
|
189
|
+
|
|
190
|
+
Who is calling, in any route or in your own MCP server:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
app.get("/api/notes", (c) => {
|
|
194
|
+
const { user, agent } = mcpCaller(c.req.raw, c.env);
|
|
195
|
+
// agent is true for a tools call that ran this route
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Returns** `McpCaller`: `{ user: McpUser | null, agent: boolean }`.
|
|
200
|
+
`user` comes from `env.USER`. Without `env`, it falls back to the identity the
|
|
201
|
+
platform attached to the request.
|
|
202
|
+
|
|
203
|
+
## When the tool list is wrong
|
|
204
|
+
|
|
205
|
+
The list is checked on the first request. A duplicate or invalid name, a tool
|
|
206
|
+
with neither or both of `handler` and `route`, a route tool without `app`, or
|
|
207
|
+
a schema that can't describe itself as JSON Schema makes the tools endpoint
|
|
208
|
+
answer `500 invalid_tools`, naming the tool. `canopy mcp add` shows that
|
|
209
|
+
message. The rest of your backend keeps working.
|
|
210
|
+
|
|
211
|
+
## Limits
|
|
212
|
+
|
|
213
|
+
`canopy mcp serve` holds back tools whose description is over 2,048
|
|
214
|
+
characters or whose input schema is over 16 KB, shows at most 64 tools, turns
|
|
215
|
+
results over 512 KB into an error, and stops a call after 60 seconds.
|
|
216
|
+
|
|
217
|
+
## Types
|
|
218
|
+
|
|
219
|
+
All exported from `@mediamonks/canopy/mcp`: `McpTool`, `McpHandlerTool`,
|
|
220
|
+
`McpRouteTool`, `McpToolAnnotations`, `McpToolContext`, `McpToolsInput`,
|
|
221
|
+
`McpToolsOptions`, `McpCaller`, `McpUser`, `AiSdkToolSet`, `ToolSchema`,
|
|
222
|
+
`JsonSchema`, `StandardSchemaLike`, `FetchApp`, `HttpMethod`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CQIPmdcO.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
4
|
import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CQIPmdcO.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
|
|
4
4
|
import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|