@extuitive/skill 0.1.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/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@extuitive/skill",
3
+ "version": "0.1.0",
4
+ "description": "The Extuitive agent skill and installer for Claude Code, Codex, and Claude Desktop. Run it with `npx extuitive`.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/fl100inc/extuitive-skill.git"
13
+ },
14
+ "homepage": "https://github.com/fl100inc/extuitive-skill#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/fl100inc/extuitive-skill/issues"
17
+ },
18
+ "keywords": [
19
+ "extuitive",
20
+ "mcp",
21
+ "claude-code",
22
+ "claude-desktop",
23
+ "codex",
24
+ "agent-skills",
25
+ "facebook-ads"
26
+ ],
27
+ "files": [
28
+ "bin",
29
+ "src",
30
+ "skills",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "doctor": "node bin/cli.mjs doctor"
39
+ }
40
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: extuitive
3
+ description: Work with Extuitive, where each workspace is one Facebook ads account. Check that Extuitive is connected and signed in, choose which ad account to work in, upload images and videos in bulk, check how an upload is going and which files were accepted, connect or repair a Meta connection, or set Extuitive up from scratch. Use when asked to check the Extuitive connection or whether Extuitive is set up, to upload, add, or import creative, ad images, ad videos, or a folder of assets into Extuitive, to check an Extuitive upload, to pick or switch which Extuitive workspace or ad account to work in, or when Extuitive tools are missing or refusing.
4
+ argument-hint: "[init | select | upload | upload-status | connect]"
5
+ arguments: command
6
+ ---
7
+
8
+ # Extuitive
9
+
10
+ One skill, five jobs. Read the reference for the job you are doing and follow it; this page
11
+ only routes.
12
+
13
+ ## Which job
14
+
15
+ The requested command is **`$command`**, from the full invocation `$ARGUMENTS`.
16
+
17
+ If that is empty, or still reads as a literal `$command` because this host does not
18
+ substitute arguments, work it out from what was actually asked instead.
19
+
20
+ | Command | Read | For |
21
+ | --- | --- | --- |
22
+ | `init` | `references/init.md` | Nothing is set up yet, the tools are missing, or "check my Extuitive connection" — is it set up, signed in, and working |
23
+ | `select` | `references/select.md` | Choose which ad account the rest of this session works in |
24
+ | `upload` | `references/upload.md` | Send local files into a workspace |
25
+ | `upload-status` | `references/upload-status.md` | How is an upload going, what was accepted |
26
+ | `connect` | `references/connect.md` | No workspaces, or ads data has gone stale — the *Meta* connection |
27
+
28
+ Anything else, or nothing at all: pick from the "For" column. An unrecognised command is
29
+ worth one sentence — say what the five are — rather than a guess.
30
+
31
+ "Connection" is ambiguous here, so route it by what is being asked about. Whether *this
32
+ host* can reach Extuitive — tools present, signed in, workspaces listed — is `init`, which
33
+ checks all three in order and hands off to `connect` itself if the only thing missing is a
34
+ Meta connection. Whether a *workspace's* ads data is flowing is `connect`.
35
+
36
+ `references/tools.md` documents every tool's arguments, the error vocabulary, and the status
37
+ lifecycle. Reach for it when a call fails or a field is not what you expected.
38
+
39
+ ## True regardless of which job
40
+
41
+ **The MCP tools never carry file bytes.** `create_upload_batch` hands back presigned storage
42
+ URLs and whoever holds the files sends the bytes there directly. No tool accepts a file. If
43
+ you cannot open the person's files, you cannot upload — use `create_browser_upload_link` and
44
+ let them do it from their browser. Being able to run code is not the same as being able to
45
+ read their disk; some hosts give you one without the other.
46
+
47
+ **Signing in happens in a browser and only the person can do it.** The token lands in this
48
+ host's own credential store. You never see it, and no amount of retrying substitutes for it.
49
+
50
+ **If the Extuitive tools are not in this session, stop and read `references/init.md`.** Do not
51
+ invent setup commands. They differ per host and they change.
52
+
53
+ **Uploading files is not an instruction to do anything with them.** Thirty images is not a
54
+ request for thirty ads. Report what landed and wait.
55
+
56
+ **Say what a tool returned, not what it implies.** The fields on a workspace do not predict
57
+ which tools will accept it — `role` and `isOwner` govern reconnecting Meta and nothing else —
58
+ so "use this one for uploads" is a claim you can only make after one worked. Two workspaces
59
+ can point at the same ad account and behave differently. Ask rather than choose.
60
+
61
+ **Ask which workspace once, then stop asking.** A choice made anywhere in the conversation —
62
+ through `select`, or in passing while asking for something else — holds until they change it
63
+ or the session ends. Re-asking on every upload is its own kind of wrong answer.
@@ -0,0 +1,75 @@
1
+ # `/extuitive connect` — connect or repair Meta
2
+
3
+ Each Extuitive workspace is one Facebook ads account. When something is wrong with that
4
+ connection, the tools tell you what and where to send the person — your job is to route to the
5
+ right one and relay what it says.
6
+
7
+ The reverse does not hold: two workspaces can point at the same ads account, each with its own
8
+ connection and its own `canReconnect`. Repairing one does not repair the other, so check the
9
+ `metaConnection` on the workspace you are actually being asked about.
10
+
11
+ ## Two different problems
12
+
13
+ They look similar and have different fixes, so identify which one you have before acting.
14
+
15
+ - **No workspaces at all.** `list_workspaces` returns an empty list. This is a setup problem,
16
+ and it belongs to the person themselves. Use `get_meta_setup_status`.
17
+ - **A workspace exists but its data has gone stale.** `list_workspaces` returns a workspace
18
+ whose `metaConnection.status` is not `healthy`. This is a repair problem, and it may belong
19
+ to someone else. Use `create_meta_reconnect_link`.
20
+
21
+ Do not guess between them. Sending someone who already finished Meta OAuth back through it
22
+ achieves nothing and wastes their time.
23
+
24
+ ## No workspaces yet
25
+
26
+ Call `get_meta_setup_status`. It takes no arguments and answers the question an empty
27
+ `list_workspaces` cannot: whether they never connected Meta, or connected it but never chose
28
+ ad accounts, or connected it and Meta returned no ad accounts.
29
+
30
+ It gives you back:
31
+
32
+ - `stage` — which of those situations this is
33
+ - `url` — the page that completes this particular stage
34
+ - `nextStep` — what to tell them, already worded for the stage
35
+
36
+ **Relay `nextStep` rather than writing your own.** It is phrased per stage precisely so that
37
+ someone who is already connected is not told to connect again.
38
+
39
+ Give them the `url`. It requires them to be signed in, so it grants nothing on its own — it is
40
+ an ordinary link to a page, not a credential. Then ask them to call you back and run
41
+ `list_workspaces` again.
42
+
43
+ ## A workspace whose connection is broken
44
+
45
+ `list_workspaces` reports `metaConnection` on every workspace:
46
+
47
+ | `status` | Means |
48
+ | --- | --- |
49
+ | `healthy` | Working. Nothing to do. |
50
+ | `expiring` | The token will expire soon; `daysUntilExpiry` says when. |
51
+ | `token_invalid` | The token stopped working. Ads data is stale until it is renewed. |
52
+ | `missing_scope` | Meta stopped granting the ads permissions this workspace needs. |
53
+ | `ad_account_permission` | Their Meta user has no role on this ad account. |
54
+
55
+ **Check `metaConnection.canReconnect` before calling `create_meta_reconnect_link`.** Only the
56
+ workspace owner can reconnect, because the Meta account is theirs. When `canReconnect` is
57
+ `false`, tell the person to ask the workspace owner — do not call the tool to find out, it
58
+ will refuse with `not_workspace_owner`.
59
+
60
+ `canReconnect` is also `false` for `ad_account_permission` even when the person *is* the
61
+ owner, because reconnecting cannot grant a Business Manager role. That one is fixed in Meta's
62
+ own settings, not in Extuitive.
63
+
64
+ When `canReconnect` is `true`, call `create_meta_reconnect_link` with the `workspaceId`. It
65
+ returns a `url` and a `nextStep`. Same rule as above: relay the `nextStep`, hand over the
66
+ `url`, and note that the page requires sign-in.
67
+
68
+ ## What not to do
69
+
70
+ Do not report numbers from a workspace whose connection is broken without saying so. A
71
+ `token_invalid` workspace still has data, but it stopped updating on the day the token died,
72
+ and presenting it as current is worse than reporting nothing.
73
+
74
+ Do not treat any of these URLs as secret. They all point at pages that require the person to
75
+ be signed in.
@@ -0,0 +1,83 @@
1
+ # `/extuitive init` — set Extuitive up, or check that it is
2
+
3
+ Gets a person from nothing to a working connection, and doubles as the check: "is Extuitive
4
+ connected?" is answered by walking the same three steps and reporting where they stop. Run
5
+ when asked to set up or check the connection, or when the Extuitive tools are missing from
6
+ the session.
7
+
8
+ Work through the obstacles in order. Each has to be true before the next can be. When asked
9
+ only to check, say which step you got to and stop there; do not start fixing what was not
10
+ asked about.
11
+
12
+ ## 1. Is this host connected to the Extuitive MCP server?
13
+
14
+ If the Extuitive tools are not available in this session, the connection is not set up.
15
+
16
+ **Some hosts have no command line at all.** If this one is a desktop app where you reached the
17
+ person through a chat window rather than a terminal — Claude Desktop is the one to expect —
18
+ then the connection is a panel they click through, and telling them to run something would
19
+ send them looking for a terminal they may not have:
20
+
21
+ > Settings, then Connectors, then Add custom connector, and paste
22
+ > `https://www.extuitive.com/mcp` as the URL.
23
+
24
+ Everywhere else, tell them to run:
25
+
26
+ ```bash
27
+ npx github:fl100inc/extuitive-skill doctor
28
+ ```
29
+
30
+ That checks the endpoint, whether this host has the server registered, and whether sign-in
31
+ has been completed, and it prints the exact next command for their host.
32
+
33
+ **Do not invent setup commands.** They differ per host, they change between versions, and
34
+ `doctor` reads the current ones. A stale command sends someone down a dead end that looks
35
+ like the product is broken. The connector URL above is the one exception, and only because
36
+ it is a URL rather than a command — there is nothing about it to go stale but the address.
37
+
38
+ Relay what `doctor` prints, in the order it prints it. Three things are worth saying plainly
39
+ while they work through it:
40
+
41
+ - **Signing in happens in a browser and only they can do it.** It opens a page, they approve
42
+ access, and the token goes into their host's credential store. You never see it.
43
+ - **Do not run the sign-in step yourself.** If `doctor` prints a slash command, it belongs to
44
+ the person's session, not your shell; if it prints a shell command, it opens a browser and
45
+ waits for a redirect your shell cannot receive. Either way, running it produces a failure
46
+ that reads as a broken install. Print the step and stop.
47
+ - **The tools will not appear in this session.** Hosts connect MCP servers when a session
48
+ starts, so nothing registered while you have been running is visible to you. (The skill
49
+ itself is different: hosts pick up new skills without a restart, which is why you are
50
+ reading this.) Say so, and ask them to start a fresh session — a new chat, in a desktop
51
+ app — before checking. Retrying the tools here will not make them exist.
52
+
53
+ ## 2. Do they have an account?
54
+
55
+ They do not need one in advance, and you should not send them off to make one first.
56
+
57
+ The sign-in page in step 1 offers **Sign up** alongside **Log in**, both with a one-time
58
+ email code. Someone with no account creates it there, in the same browser step that grants
59
+ this host access. If they have no Meta connection yet, that same page then shows a panel
60
+ pointing at the right setup step and returns them to the consent screen afterwards.
61
+
62
+ So: let the flow do it. Do not narrate the signup steps or send them to a separate page —
63
+ anything you add is a second, worse copy of a flow that already handles this.
64
+
65
+ ## 3. Does it work?
66
+
67
+ Once the tools are available, call `list_workspaces`.
68
+
69
+ - **It returns workspaces.** Setup is done. Say which ones they have and stop.
70
+ - **It returns an empty list.** They are signed in but have no ads account connected yet.
71
+ Read `connect.md` — it covers exactly this.
72
+ - **It refuses.** They are not signed in. Back to step 1.
73
+
74
+ ## What "done" means
75
+
76
+ At least one workspace from `list_workspaces`. Until then, do not start on anything else they
77
+ asked for: uploads and reporting both need a `workspaceId`, and every one of those tools will
78
+ refuse without it.
79
+
80
+ When setup started from a session with no Extuitive tools, that call belongs to the next
81
+ session, not this one. Finish by handing over the remaining steps and saying which one they
82
+ are on — not by declaring it working, and not by checking again for tools that cannot arrive
83
+ until they restart.
@@ -0,0 +1,89 @@
1
+ # `/extuitive select` — choose which ad account to work in
2
+
3
+ An Extuitive workspace is one Facebook ads account. This picks the one the rest of the
4
+ conversation uses, so the person is not asked again on every upload and every status check.
5
+
6
+ Anything they typed after the command — a name, a slug, an ad account id — arrives in
7
+ `$ARGUMENTS`.
8
+
9
+ **The choice lasts this conversation and nothing longer.** Nothing is written to disk, and no
10
+ tool remembers it. A new session starts with none, which is worth saying once when they choose
11
+ so that it is not a surprise tomorrow.
12
+
13
+ If the Extuitive tools are missing from this session, read `init.md` instead. There is nothing
14
+ to select from until they are there.
15
+
16
+ ## 1. List what they can actually reach
17
+
18
+ Call `list_workspaces`. It takes no arguments.
19
+
20
+ An **empty list** means they have no workspace, not that they lack access to one. Read
21
+ `connect.md`; there is nothing to select yet.
22
+
23
+ **Exactly one** is not a question. Say which workspace you are using and move on — a menu of
24
+ one spends a turn to tell them something they cannot change.
25
+
26
+ ## 2. Show every row, ranked by nothing
27
+
28
+ Give them each workspace with the three fields that actually tell one from another:
29
+
30
+ | Show | From | Why |
31
+ | --- | --- | --- |
32
+ | Name | `name` | The only part they will recognise |
33
+ | Ad account | `facebookAdAccountId`, or say it has none when `null` | What they think of as the account |
34
+ | Connection | `metaConnection.status`, when it is not `healthy` | A stale workspace is still selectable, and they should know that before choosing it |
35
+
36
+ **Do not order or recommend by `role` or `isOwner`.** Those govern who may reconnect Meta and
37
+ say nothing about uploading — a workspace where they are `viewer` may take files that the one
38
+ where they are `owner` refuses. Presenting one as the right one to work in is a claim you
39
+ cannot make yet.
40
+
41
+ **Two rows can carry the same `facebookAdAccountId`.** They are two separate workspaces with
42
+ separate content, not one workspace listed twice. Show both, say they point at the same ad
43
+ account, and let them pick. The ad account id does not identify a workspace, and collapsing
44
+ the two loses the distinction that decides where their files end up.
45
+
46
+ ## 3. Match what they said, or ask
47
+
48
+ When `$ARGUMENTS` names something, match it against `name`, `slug`, and
49
+ `facebookAdAccountId`, ignoring case.
50
+
51
+ - **One match.** Take it, and confirm which one by name.
52
+ - **Several.** Ask which, showing what differs between them. A shared ad account id lands
53
+ here, and it is the case where a guess costs the most.
54
+ - **None.** Say so and list what they do have, rather than matching loosely onto whichever
55
+ name looks closest.
56
+
57
+ With no argument, ask.
58
+
59
+ ## 4. Say what you selected
60
+
61
+ Confirm the workspace by **name**, not by id. The id is for the tools; the name is the part
62
+ they can check. Mention its ad account when it has one.
63
+
64
+ Then hold that `workspaceId` and pass it to every workspace-scoped call for the rest of the
65
+ conversation. Not asking again is the entire point of having asked once.
66
+
67
+ ## Selecting is not access
68
+
69
+ **A selected workspace can still refuse a call.** Membership is re-checked on every single
70
+ call, so `workspace_access_denied` can come back from a `workspaceId` that `list_workspaces`
71
+ returned a moment earlier.
72
+
73
+ When it does, the selection is wrong rather than the call. Say plainly which one was refused,
74
+ select another, and carry on in that one — correcting yourself in the same breath if you had
75
+ already told them their files were going somewhere else. Retrying the refused id will not
76
+ change it, and neither `role` nor `isOwner` explains it.
77
+
78
+ ## Switching later
79
+
80
+ People change their mind mid-conversation. Run this again and replace the selection.
81
+
82
+ Two things do not follow the switch. A `batchId` belongs to the workspace it was created in,
83
+ so checking on an older batch needs the workspace it was made in, not the current one. And
84
+ files already uploaded stay where they were sent — selecting elsewhere moves nothing.
85
+
86
+ ## Choosing without the command
87
+
88
+ Naming a workspace in passing — "upload these to Acme" — is a selection. Take it, say which
89
+ workspace you understood that to mean, and do not ask again in the same breath.
@@ -0,0 +1,252 @@
1
+ # Extuitive MCP tools
2
+
3
+ Every tool the Extuitive MCP server exposes, with its arguments and what it gives back. Read
4
+ this when you need a schema or an error meaning; the `SKILL.md` files carry the procedures.
5
+
6
+ Server endpoint is `https://www.extuitive.com/mcp`. Every tool except `list_workspaces`,
7
+ `get_meta_setup_status`, and `create_meta_reconnect_link`'s sibling takes a `workspaceId`, and
8
+ membership is re-checked on every single call — a token issued before someone left a workspace
9
+ does not still reach it.
10
+
11
+ ## Read this first: five things that cause wrong answers
12
+
13
+ 1. **`role` and `isOwner` say nothing about uploading.** They decide one thing only: who may
14
+ call `create_meta_reconnect_link`. A workspace where this caller is `viewer` may accept
15
+ uploads that the one where they are `owner` refuses. Nothing in a `list_workspaces` row
16
+ predicts which workspaces the upload tools will accept, so never present one as the right
17
+ one for uploads — ask, or try and report what happened.
18
+ 2. **`count` is not the batch size.** In `get_upload_batch_content`, `count` describes only
19
+ the rows returned after filtering. `statusCounts`, `pending`, and `settled` always describe
20
+ the whole batch. Filtering to `["READY"]` and reading `count` makes an unfinished upload
21
+ look complete.
22
+ 3. **`complete_upload` uses S3's capitalisation.** `PartNumber` and `ETag`, not `partNumber`
23
+ and `eTag`. They are the only non-camelCase fields in the entire surface.
24
+ 4. **`sign_upload_part` does not give you the checksum.** You compute
25
+ `x-amz-checksum-crc32c` yourself over the exact bytes of that part and send it as a header.
26
+ The signature covers that header, so S3 rejects the part when it is missing or wrong. The
27
+ bundled `scripts/upload.mjs` does this for you.
28
+ 5. **No tool is annotated.** None declares `readOnlyHint`, `destructiveHint`, or an output
29
+ schema, so nothing can be inferred about safety from the listing. `abort_upload` is
30
+ destructive and carries no marking.
31
+
32
+ Results always arrive as both a JSON text block and `structuredContent` with the same payload.
33
+
34
+ ## Workspaces
35
+
36
+ ### `list_workspaces`
37
+
38
+ No arguments. Start every session here.
39
+
40
+ Returns `{ workspaces: [...] }`, each with:
41
+
42
+ | Field | Meaning |
43
+ | --- | --- |
44
+ | `workspaceId` | Pass this to every other tool |
45
+ | `name`, `slug` | Display |
46
+ | `role` | This caller's role in the workspace. Governs reconnect only |
47
+ | `facebookAdAccountId` | The Meta ads account, or `null`. Not unique across workspaces |
48
+ | `isOwner` | Whether this caller owns the workspace. Governs reconnect only |
49
+ | `metaConnection.status` | `healthy`, `expiring`, `token_invalid`, `missing_scope`, `ad_account_permission` |
50
+ | `metaConnection.daysUntilExpiry` | Whole days, or `null` when Meta gave no expiry |
51
+ | `metaConnection.canReconnect` | Whether *this* caller can fix it |
52
+
53
+ An empty list means no workspace, not no access. Follow with `get_meta_setup_status`.
54
+
55
+ Two rows can carry the same `facebookAdAccountId`. They are still two separate workspaces
56
+ with separate content, not one workspace listed twice, so do not merge them or pick between
57
+ them on the person's behalf. Whichever they choose is the one their files will be in.
58
+
59
+ A row in this list is also not a guarantee that every tool will accept its `workspaceId`; see
60
+ `workspace_access_denied` below.
61
+
62
+ ## Meta connection
63
+
64
+ ### `get_meta_setup_status`
65
+
66
+ No arguments. The right follow-up to an empty `list_workspaces`.
67
+
68
+ Returns `stage`, `connected`, `discoveredAdAccountCount`, `connectedAdAccountCount`, `action`,
69
+ `url` (absent only when setup is complete), and `nextStep`. Relay `nextStep` rather than
70
+ writing your own wording — it differs per stage so that someone already connected is not told
71
+ to connect again.
72
+
73
+ ### `create_meta_reconnect_link`
74
+
75
+ | Argument | Required | Notes |
76
+ | --- | --- | --- |
77
+ | `workspaceId` | yes | Caller must **own** it, not merely belong to it |
78
+
79
+ Returns `workspaceId`, `status`, `action`, `url`, `nextStep`. Check
80
+ `metaConnection.canReconnect` first; a non-owner gets `not_workspace_owner` and the right
81
+ answer is to ask the owner instead.
82
+
83
+ ## Uploads
84
+
85
+ Bytes never pass through any of these. `create_upload_batch` mints presigned storage URLs and
86
+ the caller sends bytes to them directly.
87
+
88
+ ### `get_upload_limits`
89
+
90
+ | Argument | Required |
91
+ | --- | --- |
92
+ | `workspaceId` | yes |
93
+
94
+ Returns `multipartThresholdBytes`, `recommendedPartBytes`, `maxParts`, `maxFiles`, and
95
+ `media.image` / `media.video`, each with `maxBytes` and `contentTypes`. Image and video
96
+ ceilings differ by orders of magnitude. Never hardcode any of these.
97
+
98
+ ### `create_upload_batch`
99
+
100
+ | Argument | Required | Notes |
101
+ | --- | --- | --- |
102
+ | `workspaceId` | yes | |
103
+ | `files` | yes | 1 to `maxFiles` entries (250 when the server does not say) |
104
+ | `files[].fileName` | yes | Including extension |
105
+ | `files[].contentType` | yes | Must be an allowed type |
106
+ | `files[].bytes` | yes | Whole number, the real size |
107
+
108
+ Returns `batchId`, `metaAdAccountId`, `createdAt`, `fileCount`, and `files` — one destination
109
+ per file, in the order sent. A destination is one of:
110
+
111
+ - **PUT** — `method`, `fileName`, `contentId`, `url`, `fields`, `headers`, `expiresIn` (1800).
112
+ Send the whole file to `url` with `headers` verbatim.
113
+ - **MULTIPART** — `method`, `fileName`, `contentId`, `uploadId`, `key`, `partBytes`. Needs
114
+ `sign_upload_part` per part, then `complete_upload`.
115
+
116
+ ### `resign_upload`
117
+
118
+ | Argument | Required |
119
+ | --- | --- |
120
+ | `workspaceId`, `contentId` | yes |
121
+
122
+ A fresh presigned PUT for one file. Every URL in a batch is signed at the same instant and
123
+ lasts 1800 seconds, so late files in a large batch can expire while early ones are still
124
+ transferring. Call on a 403, or when most of `expiresIn` has passed.
125
+
126
+ ### `sign_upload_part`
127
+
128
+ | Argument | Required | Notes |
129
+ | --- | --- | --- |
130
+ | `workspaceId`, `uploadId` | yes | |
131
+ | `partNumber` | yes | 1 to 10000 |
132
+
133
+ Returns `{ url, headers, expiresIn }`. Takes no bytes and no checksum, so all parts can be
134
+ signed up front before any transfer starts. Part count is `ceil(bytes / partBytes)`.
135
+
136
+ ### `list_upload_parts`
137
+
138
+ | Argument | Required |
139
+ | --- | --- |
140
+ | `workspaceId`, `uploadId` | yes |
141
+
142
+ Returns a **bare array** of `{ PartNumber, Size, ETag }` — what storage already holds. Use it
143
+ to resume without re-sending parts that landed.
144
+
145
+ ### `complete_upload`
146
+
147
+ | Argument | Required | Notes |
148
+ | --- | --- | --- |
149
+ | `workspaceId`, `uploadId` | yes | |
150
+ | `parts` | yes | `{ PartNumber, ETag }[]`, capitalised exactly so |
151
+
152
+ Assembles the object. Returns `VALIDATING` — this does **not** mean accepted. Safe to retry.
153
+
154
+ ### `abort_upload`
155
+
156
+ | Argument | Required |
157
+ | --- | --- |
158
+ | `workspaceId`, `uploadId` | yes |
159
+
160
+ Discards a multipart upload and its stored parts. Destructive, and not marked as such.
161
+
162
+ ### `list_upload_batches`
163
+
164
+ | Argument | Required |
165
+ | --- | --- |
166
+ | `workspaceId` | yes |
167
+
168
+ Batches newest first, each with `batchId`, `createdAt`, `fileCount`, `pending`, and
169
+ `statusCounts`. No file rows. The cheap way to check on an upload, and the way to find a batch
170
+ someone created through a browser link. May return `truncated: true` at 1000 items, with no
171
+ cursor beyond that.
172
+
173
+ ### `get_upload_batch_content`
174
+
175
+ | Argument | Required | Notes |
176
+ | --- | --- | --- |
177
+ | `workspaceId`, `batchId` | yes | |
178
+ | `status` | no | Array of statuses; omit for all |
179
+ | `mediaKind` | no | `image` or `video`; omit for both |
180
+
181
+ Returns `content`, `count`, `statusCounts`, `pending`, `settled`, `batchId`. Each content row:
182
+ `contentId`, `status`, and optionally `rejectionReason`, `fileName`, `mediaKind`, `bytes`,
183
+ `contentType`, `format`, `createdAt`, `updatedAt`, and `url` on a `READY` image.
184
+
185
+ Remember that filtering narrows `content` and `count` only.
186
+
187
+ ### `get_upload_content`
188
+
189
+ | Argument | Required |
190
+ | --- | --- |
191
+ | `workspaceId`, `contentId` | yes |
192
+
193
+ One row, same shape as above. For a file uploaded on its own, which has no `batchId`.
194
+
195
+ ### `create_browser_upload_link`
196
+
197
+ | Argument | Required |
198
+ | --- | --- |
199
+ | `workspaceId` | yes |
200
+
201
+ Returns `{ workspaceId, url }` — a page where the person uploads from their browser. Use it
202
+ whenever you cannot make HTTP requests, and for very large video where signing every part
203
+ would take too many calls. The page requires them to be signed in, so the link grants nothing
204
+ on its own.
205
+
206
+ ## Status lifecycle
207
+
208
+ | Status | Final | Counted in `pending` |
209
+ | --- | --- | --- |
210
+ | `CREATED` | no | yes |
211
+ | `UPLOADING` | no | yes |
212
+ | `VALIDATING` | no | yes |
213
+ | `READY` | **yes** | no |
214
+ | `REJECTED` | yes | no |
215
+ | `ABORTED` | yes | no |
216
+ | `EXPIRED` | no | yes |
217
+
218
+ `READY` is the only status that means a file uploaded successfully. Poll every 5 seconds, give
219
+ up after 5 minutes.
220
+
221
+ ## Errors
222
+
223
+ Refusals come back as a result with `isError: true` and a payload of `{ error, message }`, so
224
+ they are yours to read and act on rather than hard failures.
225
+
226
+ | `error` | Meaning |
227
+ | --- | --- |
228
+ | `invalid_arguments` | A required argument is missing, empty, or out of range |
229
+ | `invalid_upload_body` | The `files` manifest was malformed |
230
+ | `missing_workspace_id` | `workspaceId` was blank |
231
+ | `workspace_access_denied` | Caller is not a member of that workspace |
232
+ | `not_workspace_owner` | Caller belongs to the workspace but does not own it |
233
+ | `upload_not_found` | No such content id |
234
+ | `upload_signing_unreachable` | Upstream signing service could not be reached |
235
+ | `upload_signing_timeout` | Upstream signing service timed out |
236
+ | `upload_signing_invalid_json` | Upstream returned something unparseable |
237
+ | `upload_signing_unconfigured` | Server is missing its upstream credentials |
238
+ | `upstream_{status}` | Upstream error with no more specific code |
239
+ | `internal_error` | Server-side fault; the detail is logged, not returned |
240
+
241
+ Genuine protocol faults — an unknown tool, malformed JSON-RPC — arrive as JSON-RPC errors
242
+ instead, and mean something is wrong with the call itself rather than its arguments.
243
+
244
+ ### `workspace_access_denied` on an id from `list_workspaces`
245
+
246
+ This happens, and it is not something to solve by reasoning. Membership is re-checked on every
247
+ call against the tool being called, so a workspace can appear in the list and still refuse an
248
+ upload. Retrying will not change it, and neither `role` nor `isOwner` predicts it.
249
+
250
+ Say plainly which id was refused and which one worked, and carry on with the one that worked.
251
+ If you had already told the person to use the refused one, correct that in the same breath —
252
+ they are about to look for their files in a workspace that has none.