@lanes-sh/link 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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor trimmed OpenAPI specs for Google's REST APIs.
|
|
3
|
+
*
|
|
4
|
+
* Google's MCP servers are gated behind a Workspace Developer Preview that a
|
|
5
|
+
* personal account cannot enrol in, so the REST APIs are the path that works
|
|
6
|
+
* for everyone. They publish Discovery documents rather than OpenAPI; APIs.guru
|
|
7
|
+
* generates OpenAPI from those, which is what this reads.
|
|
8
|
+
*
|
|
9
|
+
* The output is **committed**, and that is the point. A spec decides which
|
|
10
|
+
* paths get called with the operator's token, and `connect` now grants
|
|
11
|
+
* everything a provider discovers — so a spec fetched at connect time from a
|
|
12
|
+
* third party could introduce, say, a DELETE operation that lands on Google's
|
|
13
|
+
* own host holding a real mailbox token. Vendoring makes the surface reviewable
|
|
14
|
+
* and the build reproducible; this script exists so refreshing it is one
|
|
15
|
+
* command rather than a hand edit.
|
|
16
|
+
*
|
|
17
|
+
* bun run vendor:google
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { OpenAPIToolGenerator, type McpOpenAPITool } from 'mcp-from-openapi';
|
|
23
|
+
|
|
24
|
+
/** Kept in step with `BUDGET_KB` in `src/cli/tools.test.ts`, which enforces it. */
|
|
25
|
+
const BUDGET_KB = 64;
|
|
26
|
+
|
|
27
|
+
interface Spec {
|
|
28
|
+
openapi: string;
|
|
29
|
+
info: Record<string, unknown>;
|
|
30
|
+
servers?: Array<{ url: string }>;
|
|
31
|
+
paths: Record<string, Record<string, { operationId?: string } & Record<string, unknown>>>;
|
|
32
|
+
components?: { schemas?: Record<string, unknown> } & Record<string, unknown>;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The operations each provider exposes.
|
|
38
|
+
*
|
|
39
|
+
* Curated, and deliberately so: Gmail's spec has 79 operations and Drive's 48,
|
|
40
|
+
* which is more than an agent can reason over and far more than either provider
|
|
41
|
+
* claims to be. Everything here is reachable under the scopes the manifest
|
|
42
|
+
* requests — nothing is listed that would fail on permission.
|
|
43
|
+
*/
|
|
44
|
+
const SELECTION: Record<
|
|
45
|
+
string,
|
|
46
|
+
{ source: string; out: string; operations: string[]; opaque?: string[] }
|
|
47
|
+
> = {
|
|
48
|
+
gmail: {
|
|
49
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/gmail/v1/openapi.json',
|
|
50
|
+
out: 'gmail.v1.json',
|
|
51
|
+
operations: [
|
|
52
|
+
'gmail.users.getProfile',
|
|
53
|
+
'gmail.users.labels.list',
|
|
54
|
+
'gmail.users.labels.get',
|
|
55
|
+
'gmail.users.messages.list',
|
|
56
|
+
'gmail.users.messages.get',
|
|
57
|
+
'gmail.users.messages.attachments.get',
|
|
58
|
+
'gmail.users.threads.list',
|
|
59
|
+
'gmail.users.threads.get',
|
|
60
|
+
'gmail.users.drafts.list',
|
|
61
|
+
'gmail.users.drafts.get',
|
|
62
|
+
// Write, all of it `gmail.compose`, all of it in the write bundle.
|
|
63
|
+
// `delete` addresses a draft that already exists, which is the half that
|
|
64
|
+
// was missing: without it the only way to discard a draft was the Gmail
|
|
65
|
+
// UI.
|
|
66
|
+
'gmail.users.drafts.send',
|
|
67
|
+
'gmail.users.drafts.delete',
|
|
68
|
+
// Organising, all of it `gmail.modify`. Gmail has no separate verb for
|
|
69
|
+
// read-state or spam: marking read is removing `UNREAD`, marking spam is
|
|
70
|
+
// adding `SPAM`, archiving is removing `INBOX`. It is all label edits, so
|
|
71
|
+
// `modify` is the whole feature and there is no narrower scope that does
|
|
72
|
+
// it — `gmail.labels` governs the label vocabulary, not its application.
|
|
73
|
+
'gmail.users.messages.modify',
|
|
74
|
+
'gmail.users.messages.batchModify',
|
|
75
|
+
'gmail.users.messages.trash',
|
|
76
|
+
'gmail.users.messages.untrash',
|
|
77
|
+
'gmail.users.threads.modify',
|
|
78
|
+
'gmail.users.threads.trash',
|
|
79
|
+
'gmail.users.threads.untrash',
|
|
80
|
+
'gmail.users.labels.create',
|
|
81
|
+
'gmail.users.labels.update',
|
|
82
|
+
'gmail.users.labels.delete',
|
|
83
|
+
// Blocking a sender, which is the one mail verb nothing above reaches.
|
|
84
|
+
// Adding `SPAM` to a message is Report-spam and trains the classifier;
|
|
85
|
+
// Block-sender is a filter, and a filter is the only thing here that acts
|
|
86
|
+
// on mail that has not arrived yet. `create` and `delete` accept
|
|
87
|
+
// `gmail.settings.basic` and nothing else, which is why the manifest asks
|
|
88
|
+
// for it and why it is marked broad.
|
|
89
|
+
//
|
|
90
|
+
// `list` is here because a create tool with no way to see what already
|
|
91
|
+
// exists is half a feature — and it is free, accepting `gmail.readonly`,
|
|
92
|
+
// which the manifest already held. `get` is not: `list` returns every
|
|
93
|
+
// filter in full, so it is the `labels.patch` objection again.
|
|
94
|
+
'gmail.users.settings.filters.list',
|
|
95
|
+
'gmail.users.settings.filters.create',
|
|
96
|
+
'gmail.users.settings.filters.delete',
|
|
97
|
+
//
|
|
98
|
+
// Not listed, deliberately: `messages.delete` and `threads.delete` are
|
|
99
|
+
// permanent and would force `mail.google.com`, the one scope we refuse;
|
|
100
|
+
// `labels.patch` duplicates `update`, and a redundant tool costs context
|
|
101
|
+
// on every call; `messages.insert`/`import` take a full `Message` body,
|
|
102
|
+
// which is the `cutCycles` hazard below.
|
|
103
|
+
//
|
|
104
|
+
// `drafts.create` was here and is gone, which is the `labels.patch`
|
|
105
|
+
// objection with a bill attached. `send_message` already creates drafts —
|
|
106
|
+
// `draft_only: true` posts the assembled message to /drafts — so the two
|
|
107
|
+
// differed only in whether the endpoint composed the MIME for you, with
|
|
108
|
+
// nothing in the tool list saying so. The generated one took a base64url
|
|
109
|
+
// `raw` the caller had to assemble, which is the failure ADR-017 exists
|
|
110
|
+
// for: a 239 KB PDF is about 320,000 characters a model cannot write in
|
|
111
|
+
// one message. Two tools for one job, one of which cannot attach a file.
|
|
112
|
+
// The test in `specs.test.ts` that was supposed to catch this only knew
|
|
113
|
+
// about `messages.send`; it knows about drafts now.
|
|
114
|
+
//
|
|
115
|
+
// `drafts.update` followed it out, one change later and for exactly the
|
|
116
|
+
// same reason — it was kept at first only because nothing else could
|
|
117
|
+
// revise a draft, and that stopped being true when `send_message` grew
|
|
118
|
+
// `draft_id`. The `Draft` body is the same unusable shape either way:
|
|
119
|
+
// workable for a line of text, impossible for the attachment that is the
|
|
120
|
+
// whole reason someone is correcting a draft rather than retyping it.
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
drive: {
|
|
124
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/drive/v3/openapi.json',
|
|
125
|
+
out: 'drive.v3.json',
|
|
126
|
+
operations: [
|
|
127
|
+
'drive.about.get',
|
|
128
|
+
'drive.files.list',
|
|
129
|
+
'drive.files.get',
|
|
130
|
+
'drive.files.export',
|
|
131
|
+
'drive.permissions.list',
|
|
132
|
+
// Write, and every one of them limited to files this app created by the
|
|
133
|
+
// `drive.file` scope. That bound is why there is no rename-anything tool
|
|
134
|
+
// here: `drive.file`'s other half is files the user picks, which arrives
|
|
135
|
+
// through the Google Picker, and there is no picker on an MCP endpoint.
|
|
136
|
+
'drive.files.create',
|
|
137
|
+
'drive.files.update',
|
|
138
|
+
'drive.files.copy',
|
|
139
|
+
'drive.permissions.create',
|
|
140
|
+
//
|
|
141
|
+
// Not listed, deliberately: `files.delete` is permanent, and Drive has a
|
|
142
|
+
// trash — `files.update` with `trashed: true` is the recoverable form of
|
|
143
|
+
// the same intent, which is the `messages.delete` reasoning again.
|
|
144
|
+
// `permissions.update` and `permissions.delete` would let an agent revoke
|
|
145
|
+
// access it did not grant, including somebody else's.
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
//
|
|
149
|
+
// Sheets and Docs exist because Drive cannot do this. Drive treats a Google
|
|
150
|
+
// file as an opaque blob: there is no Drive operation that edits a cell, and
|
|
151
|
+
// the only route back in would be `files.update` with media — a whole-file
|
|
152
|
+
// replace via import conversion, which discards formulas, formatting, tabs,
|
|
153
|
+
// and comments. Cell-level editing lives solely in the Sheets API, and
|
|
154
|
+
// paragraph-level editing solely in the Docs API.
|
|
155
|
+
sheets: {
|
|
156
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/sheets/v4/openapi.json',
|
|
157
|
+
out: 'sheets.v4.json',
|
|
158
|
+
opaque: ['Request', 'Response'],
|
|
159
|
+
operations: [
|
|
160
|
+
'sheets.spreadsheets.get',
|
|
161
|
+
'sheets.spreadsheets.values.get',
|
|
162
|
+
'sheets.spreadsheets.values.batchGet',
|
|
163
|
+
// Write. `values.*` is the whole point of the provider — reading a range
|
|
164
|
+
// and writing it back is what "work on the file directly" means.
|
|
165
|
+
'sheets.spreadsheets.values.update',
|
|
166
|
+
'sheets.spreadsheets.values.batchUpdate',
|
|
167
|
+
'sheets.spreadsheets.values.append',
|
|
168
|
+
'sheets.spreadsheets.values.clear',
|
|
169
|
+
// The N-ary form of `values.clear`, and here for consistency rather than
|
|
170
|
+
// reach: `values.batchGet` and `values.batchUpdate` are both already the
|
|
171
|
+
// batch forms of vendored unary operations, so refusing this one alone
|
|
172
|
+
// was an inconsistency with no argument behind it. It also collapses
|
|
173
|
+
// clearing five tabs from five audit events into one that names all five
|
|
174
|
+
// ranges. Measured at 0.6 KB.
|
|
175
|
+
'sheets.spreadsheets.values.batchClear',
|
|
176
|
+
// Copying a tab into a *different* spreadsheet, which nothing else here
|
|
177
|
+
// can do: `batchUpdate`'s `DuplicateSheetRequest` cannot cross a file
|
|
178
|
+
// boundary, and `drive.files.copy` copies the whole file. Not the
|
|
179
|
+
// `labels.patch` objection — that refused a redundant tool, and this is
|
|
180
|
+
// reachable no other way. Measured at 0.7 KB.
|
|
181
|
+
'sheets.spreadsheets.sheets.copyTo',
|
|
182
|
+
// Structural edits — tabs, formatting, frozen rows, charts. Only reachable
|
|
183
|
+
// at all because `Request` is made opaque below; see `makeOpaque`.
|
|
184
|
+
'sheets.spreadsheets.batchUpdate',
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
docs: {
|
|
188
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/docs/v1/openapi.json',
|
|
189
|
+
out: 'docs.v1.json',
|
|
190
|
+
opaque: ['Request', 'Response'],
|
|
191
|
+
operations: [
|
|
192
|
+
'docs.documents.get',
|
|
193
|
+
// The only way to edit a document at all. Docs has no `values`-style
|
|
194
|
+
// shortcut, so this operation is the entire write surface.
|
|
195
|
+
'docs.documents.batchUpdate',
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
calendar: {
|
|
199
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.json',
|
|
200
|
+
out: 'calendar.v3.json',
|
|
201
|
+
operations: [
|
|
202
|
+
'calendar.calendarList.list',
|
|
203
|
+
'calendar.events.list',
|
|
204
|
+
'calendar.events.get',
|
|
205
|
+
'calendar.events.instances',
|
|
206
|
+
// Reading, but a POST: `freeBusy` takes a body because it asks about
|
|
207
|
+
// several calendars at once. It is the "when am I free" primitive, and
|
|
208
|
+
// the reason `calendar.readonly` is requested beside `calendar.events` —
|
|
209
|
+
// this operation and `calendarList.list` accept nothing narrower.
|
|
210
|
+
'calendar.freebusy.query',
|
|
211
|
+
// Write, all of it `calendar.events`. Nothing here needs full `calendar`.
|
|
212
|
+
'calendar.events.insert',
|
|
213
|
+
'calendar.events.patch',
|
|
214
|
+
'calendar.events.delete',
|
|
215
|
+
'calendar.events.move',
|
|
216
|
+
//
|
|
217
|
+
// Not listed, deliberately. `events.update` is the PUT beside `patch`,
|
|
218
|
+
// and this is the inverse of the `labels.patch` decision above: there the
|
|
219
|
+
// two were the same call and the redundant one went, here they differ and
|
|
220
|
+
// the difference destroys data. PUT replaces the resource, so an agent
|
|
221
|
+
// that reads an event, edits `summary`, and sends it back drops
|
|
222
|
+
// everything it did not echo — attendees, reminders, recurrence,
|
|
223
|
+
// conferencing. One of them is needed and only one of them is safe.
|
|
224
|
+
// `events.quickAdd` parses "lunch with Bob Tuesday" into an event, which
|
|
225
|
+
// is a service for a caller that cannot do that itself; a model can, and
|
|
226
|
+
// `insert` takes the end time and time zone that quickAdd has to guess.
|
|
227
|
+
// `events.import`, `calendars.*`, `calendarList.insert`/`delete` and
|
|
228
|
+
// `acl.*` create, delete, or re-share whole calendars, and every one of
|
|
229
|
+
// them requires the full `calendar` scope — the scope this provider
|
|
230
|
+
// exists to avoid. The `watch` operations push to a webhook this endpoint
|
|
231
|
+
// does not have. `colors.get` and `settings.list` answer nothing anyone
|
|
232
|
+
// asked: the primary calendar's time zone is already in `calendarList`.
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
tasks: {
|
|
236
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/tasks/v1/openapi.json',
|
|
237
|
+
out: 'tasks.v1.json',
|
|
238
|
+
operations: [
|
|
239
|
+
'tasks.tasklists.list',
|
|
240
|
+
'tasks.tasks.list',
|
|
241
|
+
'tasks.tasks.get',
|
|
242
|
+
// Write. Tasks publishes exactly two scopes — `tasks` and
|
|
243
|
+
// `tasks.readonly` — so every line below is the same grant as every
|
|
244
|
+
// other, and there is no narrower one to prefer.
|
|
245
|
+
'tasks.tasklists.insert',
|
|
246
|
+
'tasks.tasklists.patch',
|
|
247
|
+
'tasks.tasks.insert',
|
|
248
|
+
'tasks.tasks.patch',
|
|
249
|
+
'tasks.tasks.delete',
|
|
250
|
+
'tasks.tasks.move',
|
|
251
|
+
//
|
|
252
|
+
// Not listed, deliberately: `tasklists.delete` destroys a list and every
|
|
253
|
+
// task in it, and Tasks has no trash — the `messages.delete` refusal
|
|
254
|
+
// applied to the container rather than the item. `tasks.clear` hides
|
|
255
|
+
// every completed task in a list in one call, naming none of them, so the
|
|
256
|
+
// audit entry could not say what it did. `tasklists.update` and
|
|
257
|
+
// `tasks.update` are the PUTs beside the PATCHes; see `events.update`.
|
|
258
|
+
// `tasklists.get` is `tasklists.list` with an argument.
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
//
|
|
262
|
+
// `contacts` rather than `people`: the API is People, but the thing an
|
|
263
|
+
// operator connects is their contacts, and `icloud_contacts` is already the
|
|
264
|
+
// neighbour. It is the one key here whose name differs from the API it
|
|
265
|
+
// vendors, which matters in `redact.ts` — see the note there.
|
|
266
|
+
contacts: {
|
|
267
|
+
source: 'https://api.apis.guru/v2/specs/googleapis.com/people/v1/openapi.json',
|
|
268
|
+
out: 'people.v1.json',
|
|
269
|
+
operations: [
|
|
270
|
+
'people.people.searchContacts',
|
|
271
|
+
'people.people.getBatchGet',
|
|
272
|
+
// A second store, and a second scope. "Other contacts" is where Gmail
|
|
273
|
+
// files an address written to but never saved, which for "email Bob" is
|
|
274
|
+
// more often than not where Bob actually is.
|
|
275
|
+
'people.otherContacts.search',
|
|
276
|
+
//
|
|
277
|
+
// Not listed, and this one is not a judgement call: `people.people.get`
|
|
278
|
+
// and `people.people.connections.list` cannot work through this
|
|
279
|
+
// connector. Google's discovery document writes their paths as
|
|
280
|
+
// `v1/{+resourceName}` — RFC 6570 reserved expansion, which permits the
|
|
281
|
+
// slash that every value has (`people/me`, `people/c8891…`). The OpenAPI
|
|
282
|
+
// conversion drops the `+`, and `buildPath` percent-encodes a path value
|
|
283
|
+
// as a single segment, which is correct for a plain `{var}` and fatal
|
|
284
|
+
// here. What goes out is `/v1/people%2Fme`, and Google's frontend answers
|
|
285
|
+
// 404 with an HTML page; the same URL with a literal slash reaches the
|
|
286
|
+
// API and answers 403 for the missing credential. Measured against the
|
|
287
|
+
// live service, not reasoned. Both would list fine, pass policy, and fail
|
|
288
|
+
// every call — so the enumeration of every contact is deliberately absent
|
|
289
|
+
// and `searchContacts` is how a name becomes an address.
|
|
290
|
+
//
|
|
291
|
+
// Also not listed: everything under the `contacts` write scope, and
|
|
292
|
+
// `directory.readonly`, which is a Workspace directory and a 403 on a
|
|
293
|
+
// personal account. `contactGroups.*` only helps filter an enumeration
|
|
294
|
+
// this provider does not have.
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
//
|
|
298
|
+
// Not listed for either, deliberately: `spreadsheets.create` and
|
|
299
|
+
// `documents.create`. `drive.files.create` already makes an empty Google-native
|
|
300
|
+
// file by `mimeType` under scopes we hold anyway, and their request bodies are
|
|
301
|
+
// whole `Spreadsheet`/`Document` objects — the same hazard class as the
|
|
302
|
+
// `messages.insert` rejected above.
|
|
303
|
+
//
|
|
304
|
+
// This used to say "122 and 101 extra schemas", which is the wrong number to
|
|
305
|
+
// argue with and nearly got the decision reversed: a schema count reads
|
|
306
|
+
// arguable, and the budget in `cli/tools.test.ts` is 64 KB of *generated*
|
|
307
|
+
// input schema. Those differ by 40× here, because `mcp-from-openapi` inlines
|
|
308
|
+
// `$ref`s and a shared schema is duplicated at every use site. Measured with
|
|
309
|
+
// the real generator, `spreadsheets.create` is **1,133 KB** — 17.7× the
|
|
310
|
+
// budget. `opaque: ['Request','Response']` does nothing for it: the body fans
|
|
311
|
+
// out `Spreadsheet → Sheet → GridData → CellData → CellFormat/ChartSpec` and
|
|
312
|
+
// `Spreadsheet → SpreadsheetProperties → CellFormat/SpreadsheetTheme`, and
|
|
313
|
+
// neither path passes through `Request`.
|
|
314
|
+
//
|
|
315
|
+
// Adding `Sheet` to `opaque` gets it to 80.7 KB, still over. The only
|
|
316
|
+
// configuration that fits also makes `SpreadsheetProperties` opaque — at
|
|
317
|
+
// which point the tool cannot describe `title`, and it is a tool that tells
|
|
318
|
+
// an agent nothing for a job `drive.files.create` does with typed `name`,
|
|
319
|
+
// `mimeType`, and `parents`, uniformly for Sheets, Docs, and folders.
|
|
320
|
+
// `documents.create` is the same shape at 98.1 KB, and Google ignores every
|
|
321
|
+
// field but `title` on it anyway.
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** Every `$ref` target reachable from a value, transitively. */
|
|
325
|
+
function referenced(root: unknown, schemas: Record<string, unknown>): Set<string> {
|
|
326
|
+
const found = new Set<string>();
|
|
327
|
+
const queue: unknown[] = [root];
|
|
328
|
+
|
|
329
|
+
while (queue.length > 0) {
|
|
330
|
+
const node = queue.pop();
|
|
331
|
+
if (node === null || typeof node !== 'object') continue;
|
|
332
|
+
|
|
333
|
+
if (Array.isArray(node)) {
|
|
334
|
+
queue.push(...node);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const [key, value] of Object.entries(node)) {
|
|
339
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
340
|
+
const name = value.replace('#/components/schemas/', '');
|
|
341
|
+
if (!found.has(name) && name in schemas) {
|
|
342
|
+
found.add(name);
|
|
343
|
+
queue.push(schemas[name]);
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
queue.push(value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return found;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Cut reference cycles, replacing the back-edge with an open object.
|
|
356
|
+
*
|
|
357
|
+
* Gmail's `MessagePart` contains `MessagePart[]` — a MIME tree, so the
|
|
358
|
+
* recursion is honest — and the OpenAPI tool generator inlines `$ref`s, so it
|
|
359
|
+
* recurses until the stack ends. That silently costs the two draft-writing
|
|
360
|
+
* operations, which are the useful half of `gmail.compose`.
|
|
361
|
+
*
|
|
362
|
+
* Cutting the back-edge rather than dropping the operation keeps the tool: the
|
|
363
|
+
* field that matters for creating a draft is `raw`, an RFC 2822 message, and
|
|
364
|
+
* nothing below the cut is required to fill it in. A depth-first walk marks the
|
|
365
|
+
* names currently on the path, and any `$ref` reaching back to one of them
|
|
366
|
+
* becomes a plain object.
|
|
367
|
+
*/
|
|
368
|
+
function cutCycles(schemas: Record<string, unknown>): number {
|
|
369
|
+
let cuts = 0;
|
|
370
|
+
|
|
371
|
+
const walk = (node: unknown, path: Set<string>): void => {
|
|
372
|
+
if (node === null || typeof node !== 'object') return;
|
|
373
|
+
|
|
374
|
+
if (Array.isArray(node)) {
|
|
375
|
+
for (const item of node) walk(item, path);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const record = node as Record<string, unknown>;
|
|
380
|
+
for (const [key, value] of Object.entries(record)) {
|
|
381
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
382
|
+
const name = value.replace('#/components/schemas/', '');
|
|
383
|
+
if (path.has(name)) {
|
|
384
|
+
delete record['$ref'];
|
|
385
|
+
record['type'] = 'object';
|
|
386
|
+
record['additionalProperties'] = true;
|
|
387
|
+
record['description'] = `A nested ${name}. Structure omitted: it recurses.`;
|
|
388
|
+
cuts++;
|
|
389
|
+
} else if (name in schemas) {
|
|
390
|
+
walk(schemas[name], new Set([...path, name]));
|
|
391
|
+
}
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
walk(value, path);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
for (const [name, schema] of Object.entries(schemas)) walk(schema, new Set([name]));
|
|
399
|
+
return cuts;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Replace a named schema with an open object.
|
|
404
|
+
*
|
|
405
|
+
* Same device as `cutCycles` and a different disease. That one cuts recursion;
|
|
406
|
+
* this one cuts *fan-out*. `mcp-from-openapi` inlines `$ref`s, so a schema's
|
|
407
|
+
* cost to us is not its size in the document but its size once every reference
|
|
408
|
+
* below it has been expanded — and Sheets' `Request` is a union of some eighty
|
|
409
|
+
* variants, each with its own nested grid, filter, and chart schemas, sharing
|
|
410
|
+
* sub-schemas that inlining duplicates per occurrence.
|
|
411
|
+
*
|
|
412
|
+
* Measured: `sheets.spreadsheets.batchUpdate` generates a 2,469KB input schema,
|
|
413
|
+
* against 45KB for the whole of Drive. That is unusable — it would be sent on
|
|
414
|
+
* every `tools/list` — and it is also the only operation that can add a tab,
|
|
415
|
+
* freeze a header, or format a cell, so dropping it is no better.
|
|
416
|
+
*
|
|
417
|
+
* Opaque keeps the operation and costs 3KB. The agent fills in `requests` from
|
|
418
|
+
* the API it already knows, which is the same bet `raw` makes for a Gmail draft:
|
|
419
|
+
* a well-known wire format is cheaper described than schematised. The pointer to
|
|
420
|
+
* Google's reference is in the description because that is the only thing lost.
|
|
421
|
+
*
|
|
422
|
+
* Applied before `referenced`, so the schemas that were reachable only through
|
|
423
|
+
* the replaced one leave the document entirely rather than lingering unused.
|
|
424
|
+
*/
|
|
425
|
+
function makeOpaque(schemas: Record<string, unknown>, names: readonly string[]): number {
|
|
426
|
+
let replaced = 0;
|
|
427
|
+
|
|
428
|
+
for (const name of names) {
|
|
429
|
+
if (!(name in schemas)) continue;
|
|
430
|
+
const original = schemas[name] as { description?: string };
|
|
431
|
+
schemas[name] = {
|
|
432
|
+
type: 'object',
|
|
433
|
+
additionalProperties: true,
|
|
434
|
+
description:
|
|
435
|
+
`${original.description ?? `A ${name}.`} Structure omitted: it expands to megabytes ` +
|
|
436
|
+
'when inlined. Pass the object as documented at https://developers.google.com/workspace.',
|
|
437
|
+
};
|
|
438
|
+
replaced++;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return replaced;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Google's system parameters, dropped from every operation.
|
|
446
|
+
*
|
|
447
|
+
* Three reasons, in descending order of how badly they bite.
|
|
448
|
+
*
|
|
449
|
+
* `$.xgafv` is not a legal tool-property name: the Anthropic API requires
|
|
450
|
+
* `^[a-zA-Z0-9_.-]{1,64}$`, and `$` fails it — so a single tool carrying it
|
|
451
|
+
* rejects the *entire* tools list with a 400, and every provider on the
|
|
452
|
+
* endpoint stops working, not just Gmail.
|
|
453
|
+
*
|
|
454
|
+
* `access_token`, `key`, and `oauth_token` let a caller supply their own
|
|
455
|
+
* credentials as query parameters. Authentication belongs to the connector,
|
|
456
|
+
* which sets a header; offering an agent a second way to authenticate is
|
|
457
|
+
* offering it a way to authenticate as something else.
|
|
458
|
+
*
|
|
459
|
+
* The rest is noise that costs schema for nothing.
|
|
460
|
+
*
|
|
461
|
+
* `fields` and `alt` are deliberately kept: `drive.about.get` *requires*
|
|
462
|
+
* `fields`, and `alt=media` is how file content is downloaded.
|
|
463
|
+
*/
|
|
464
|
+
const SYSTEM_PARAMETERS = new Set([
|
|
465
|
+
'$.xgafv',
|
|
466
|
+
'access_token',
|
|
467
|
+
'callback',
|
|
468
|
+
'key',
|
|
469
|
+
'oauth_token',
|
|
470
|
+
'prettyPrint',
|
|
471
|
+
'quotaUser',
|
|
472
|
+
'upload_protocol',
|
|
473
|
+
'uploadType',
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Strip system parameters from a path item or an operation.
|
|
478
|
+
*
|
|
479
|
+
* They arrive as `$ref`s into `components.parameters`, and the component *key*
|
|
480
|
+
* is not the parameter name — `$.xgafv` is keyed `_.xgafv`, because a `$` is
|
|
481
|
+
* awkward in a JSON pointer. Matching on the key would therefore miss exactly
|
|
482
|
+
* the one that breaks everything, so the ref is resolved first.
|
|
483
|
+
*/
|
|
484
|
+
function dropSystemParameters(
|
|
485
|
+
holder: Record<string, unknown>,
|
|
486
|
+
components: Record<string, unknown>,
|
|
487
|
+
): number {
|
|
488
|
+
const parameters = holder['parameters'];
|
|
489
|
+
if (!Array.isArray(parameters)) return 0;
|
|
490
|
+
|
|
491
|
+
const nameOf = (parameter: unknown): string | undefined => {
|
|
492
|
+
const record = parameter as { name?: string; $ref?: string };
|
|
493
|
+
if (record.name) return record.name;
|
|
494
|
+
if (!record.$ref) return undefined;
|
|
495
|
+
|
|
496
|
+
const key = record.$ref.replace('#/components/parameters/', '');
|
|
497
|
+
return (components[key] as { name?: string } | undefined)?.name;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const kept = parameters.filter((parameter) => {
|
|
501
|
+
const name = nameOf(parameter);
|
|
502
|
+
return !(name && SYSTEM_PARAMETERS.has(name));
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
holder['parameters'] = kept;
|
|
506
|
+
return parameters.length - kept.length;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
|
510
|
+
|
|
511
|
+
async function vendor(id: string): Promise<void> {
|
|
512
|
+
const { source, out, operations, opaque } = SELECTION[id]!;
|
|
513
|
+
const wanted = new Set(operations);
|
|
514
|
+
|
|
515
|
+
const response = await fetch(source);
|
|
516
|
+
if (!response.ok) throw new Error(`${source}: HTTP ${response.status}`);
|
|
517
|
+
const spec = (await response.json()) as Spec;
|
|
518
|
+
|
|
519
|
+
const paths: Spec['paths'] = {};
|
|
520
|
+
const seen = new Set<string>();
|
|
521
|
+
|
|
522
|
+
for (const [path, item] of Object.entries(spec.paths)) {
|
|
523
|
+
const kept: Record<string, unknown> = {};
|
|
524
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
525
|
+
// Path-level keys are not operations and must survive: `parameters` here
|
|
526
|
+
// is where Google puts the query parameters shared by every method on the
|
|
527
|
+
// path, and `fields` is one of them — which `drive.about.get` *requires*.
|
|
528
|
+
// Dropping it produced a tool with no arguments at all and a 400 on every
|
|
529
|
+
// call.
|
|
530
|
+
if (!METHODS.includes(method)) {
|
|
531
|
+
kept[method] = operation;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const operationId = operation.operationId;
|
|
535
|
+
if (!operationId || !wanted.has(operationId)) continue;
|
|
536
|
+
kept[method] = operation;
|
|
537
|
+
seen.add(operationId);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Only path-level keys survived, so no operation here was wanted.
|
|
541
|
+
if (!Object.keys(kept).some((key) => METHODS.includes(key))) continue;
|
|
542
|
+
if (Object.keys(kept).length > 0) paths[path] = kept as Spec['paths'][string];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const componentParameters = (spec.components?.['parameters'] ?? {}) as Record<string, unknown>;
|
|
546
|
+
|
|
547
|
+
let dropped = 0;
|
|
548
|
+
for (const item of Object.values(paths)) {
|
|
549
|
+
dropped += dropSystemParameters(item as unknown as Record<string, unknown>, componentParameters);
|
|
550
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
551
|
+
if (!METHODS.includes(method)) continue;
|
|
552
|
+
dropped += dropSystemParameters(
|
|
553
|
+
operation as unknown as Record<string, unknown>,
|
|
554
|
+
componentParameters,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Drop response schemas.
|
|
560
|
+
//
|
|
561
|
+
// Two reasons, and the second is the blocking one. Nothing reads them: the
|
|
562
|
+
// connector hands the response body back as text, because an agent wants the
|
|
563
|
+
// JSON, not a validated shape. And Gmail's response schemas are recursive —
|
|
564
|
+
// `Message.payload` is a `MessagePart`, which contains `MessagePart[]` — which
|
|
565
|
+
// sends the OpenAPI tool generator into infinite recursion and silently drops
|
|
566
|
+
// eight of Gmail's twelve operations from the tool list.
|
|
567
|
+
for (const item of Object.values(paths)) {
|
|
568
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
569
|
+
if (!METHODS.includes(method)) continue;
|
|
570
|
+
(operation as { responses?: unknown }).responses = {
|
|
571
|
+
'200': { description: 'Success. The response body is returned verbatim.' },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const missing = operations.filter((operationId) => !seen.has(operationId));
|
|
577
|
+
if (missing.length > 0) {
|
|
578
|
+
// Loudly, rather than shipping a provider quietly missing capabilities: an
|
|
579
|
+
// upstream rename should fail the refresh, not shrink the tool list.
|
|
580
|
+
throw new Error(`${id}: these operations are not in the spec — ${missing.join(', ')}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const schemas = spec.components?.schemas ?? {};
|
|
584
|
+
const opaqued = makeOpaque(schemas, opaque ?? []);
|
|
585
|
+
const keep = referenced(paths, schemas);
|
|
586
|
+
const trimmedSchemas = Object.fromEntries(
|
|
587
|
+
Object.entries(schemas).filter(([name]) => keep.has(name)),
|
|
588
|
+
);
|
|
589
|
+
const cuts = cutCycles(trimmedSchemas);
|
|
590
|
+
|
|
591
|
+
const trimmed: Spec = {
|
|
592
|
+
openapi: spec.openapi,
|
|
593
|
+
info: {
|
|
594
|
+
...spec.info,
|
|
595
|
+
'x-vendored-from': source,
|
|
596
|
+
'x-vendored-note':
|
|
597
|
+
'Trimmed by src/providers/google/specs/vendor.ts. Committed deliberately: a spec decides which paths are called with the operator token, so it must be reviewable rather than fetched at connect time.',
|
|
598
|
+
},
|
|
599
|
+
...(spec.servers ? { servers: spec.servers } : {}),
|
|
600
|
+
paths,
|
|
601
|
+
components: { ...spec.components, schemas: trimmedSchemas },
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// `import.meta.dir` *is* the specs directory — this script lives beside what it
|
|
605
|
+
// writes. It used to be `scripts/vendor-google-specs.ts` and reached across the
|
|
606
|
+
// tree; commit 3cd03ce moved the script and the specs together but not the path
|
|
607
|
+
// arithmetic, so for a while this resolved to
|
|
608
|
+
// `src/providers/google/providers/builtin/specs`, created it, reported success,
|
|
609
|
+
// and left the committed spec untouched — making the documented "re-run the
|
|
610
|
+
// script" step a silent no-op.
|
|
611
|
+
const directory = import.meta.dir;
|
|
612
|
+
await mkdir(directory, { recursive: true });
|
|
613
|
+
await writeFile(join(directory, out), `${JSON.stringify(trimmed, null, 2)}\n`);
|
|
614
|
+
|
|
615
|
+
const size = Math.round(JSON.stringify(trimmed).length / 1024);
|
|
616
|
+
console.log(
|
|
617
|
+
` ${id.padEnd(6)} ${String(Object.keys(paths).length).padStart(2)} paths, ` +
|
|
618
|
+
`${seen.size} operations, ${Object.keys(trimmedSchemas).length} schemas, ` +
|
|
619
|
+
`${cuts} cycle${cuts === 1 ? '' : 's'} cut, ${opaqued} made opaque, ` +
|
|
620
|
+
`${dropped} system params dropped, ${size}KB`,
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
await reportLargestTools(trimmed);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The number the budget is actually about.
|
|
628
|
+
*
|
|
629
|
+
* Everything on the line above counts the *spec*; `cli/tools.test.ts` measures
|
|
630
|
+
* the **generated input schema**, and the two differ by orders of magnitude
|
|
631
|
+
* because `mcp-from-openapi` inlines `$ref`s — a schema shared by ten fields is
|
|
632
|
+
* ten copies once generated. Reasoning about `spreadsheets.create` from a schema
|
|
633
|
+
* count put it at 122 KB when the real figure was 1,133 KB, which is the whole
|
|
634
|
+
* difference between "just over" and "seventeen times over".
|
|
635
|
+
*
|
|
636
|
+
* Printed here so the refresh that adds an operation shows its cost, rather than
|
|
637
|
+
* leaving it to a test failure to say so after the fact.
|
|
638
|
+
*/
|
|
639
|
+
async function reportLargestTools(trimmed: Spec): Promise<void> {
|
|
640
|
+
try {
|
|
641
|
+
const generator = await OpenAPIToolGenerator.fromJSON(trimmed);
|
|
642
|
+
const measured = (await generator.generateTools())
|
|
643
|
+
.map((tool: McpOpenAPITool) => ({
|
|
644
|
+
name: tool.metadata.operationId ?? tool.name,
|
|
645
|
+
kb: JSON.stringify(tool.inputSchema).length / 1024,
|
|
646
|
+
}))
|
|
647
|
+
.sort((a, b) => b.kb - a.kb);
|
|
648
|
+
|
|
649
|
+
for (const { name, kb } of measured.slice(0, 3)) {
|
|
650
|
+
const over = kb > BUDGET_KB ? ` ✗ over the ${BUDGET_KB}KB budget` : '';
|
|
651
|
+
console.log(` ${name.padEnd(38)} ${kb.toFixed(1).padStart(8)}KB${over}`);
|
|
652
|
+
}
|
|
653
|
+
} catch (error) {
|
|
654
|
+
// A generator failure is a real problem, but it is `tools.test.ts`'s to
|
|
655
|
+
// report against every provider at once. Refusing to finish the vendoring
|
|
656
|
+
// over it would leave the specs half-written.
|
|
657
|
+
console.log(` (could not measure generated schemas: ${String(error)})`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const id of Object.keys(SELECTION)) await vendor(id);
|