@lanes-sh/link 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +135 -11
- package/package.json +3 -1
- package/src/cli/argv.ts +52 -0
- package/src/cli/commands/connect/authorise.ts +5 -0
- package/src/cli/commands/connect/custom/ask.ts +167 -0
- package/src/cli/commands/connect/custom/credential.ts +143 -0
- package/src/cli/commands/connect/custom/derive.ts +229 -0
- package/src/cli/commands/connect/custom/index.ts +285 -0
- package/src/cli/commands/connect/custom/prompts.ts +160 -0
- package/src/cli/commands/connect/custom/spec.ts +293 -0
- package/src/cli/commands/connect/custom/values.ts +53 -0
- package/src/cli/commands/connect/custom/write.ts +166 -0
- package/src/cli/commands/connect/grant.ts +27 -0
- package/src/cli/commands/connect/index.ts +24 -27
- package/src/cli/commands/connect/outcome.ts +3 -1
- package/src/cli/commands/connect/requirements.ts +11 -1
- package/src/cli/commands/connect/settle.ts +17 -0
- package/src/cli/commands/connect/setup.ts +9 -1
- package/src/cli/commands/connect/strategy.ts +87 -0
- package/src/cli/commands/connect/unknown.ts +41 -0
- package/src/cli/identity.ts +29 -5
- package/src/cli/main.ts +37 -13
- package/src/cli/oauth.ts +89 -36
- package/src/cli/runtime/open.ts +13 -1
- package/src/cli/runtime/registry.ts +12 -0
- package/src/cli/selection.ts +12 -0
- package/src/cli/usage.ts +9 -1
- package/src/connectivity/auth/README.md +8 -1
- package/src/connectivity/auth/strategy/index.ts +128 -4
- package/src/connectivity/connector.ts +11 -0
- package/src/connectivity/index.ts +11 -1
- package/src/connectivity/manifest/auth.ts +19 -0
- package/src/connectivity/manifest/connector.ts +21 -0
- package/src/connectivity/manifest/primitives.ts +5 -1
- package/src/connectivity/manifest/provider.ts +30 -12
- package/src/connectivity/provider.ts +55 -0
- package/src/connectivity/transports/factory.ts +1 -0
- package/src/connectivity/transports/http/index.ts +73 -2
- package/src/dispatch/dispatch.ts +44 -5
- package/src/providers/bunq/hints.ts +43 -0
- package/src/providers/bunq/index.ts +87 -0
- package/src/providers/bunq/redact.ts +64 -0
- package/src/providers/bunq/specs/bunq.v1.json +864 -0
- package/src/providers/bunq/specs/vendor.ts +338 -0
- package/src/providers/bunq/strategy/handshake.ts +211 -0
- package/src/providers/bunq/strategy/index.ts +298 -0
- package/src/providers/bunq/strategy/keys.ts +72 -0
- package/src/providers/custom/index.ts +1 -6
- package/src/providers/custom/load.ts +56 -14
- package/src/providers/custom/template.ts +1 -1
- package/src/providers/discord/hints.ts +195 -0
- package/src/providers/discord/index.ts +121 -0
- package/src/providers/discord/redact.ts +99 -0
- package/src/providers/discord/specs/discord.v10.json +2333 -0
- package/src/providers/discord/specs/vendor.ts +164 -0
- package/src/providers/google/specs/vendor.ts +32 -317
- package/src/providers/index.ts +9 -0
- package/src/providers/reddit/index.ts +113 -0
- package/src/providers/reddit/oauth.ts +77 -0
- package/src/providers/reddit/redact.ts +33 -0
- package/src/providers/reddit/scopes.ts +27 -0
- package/src/providers/reddit/specs/reddit.v1.json +700 -0
- package/src/providers/scopes.ts +2 -0
- package/src/providers/shared/openapi.ts +155 -0
- package/src/providers/shared/vendor-operations.ts +98 -0
- package/src/providers/shared/vendor-spec.ts +309 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor a trimmed OpenAPI spec for Discord's v10 HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Discord publishes an OpenAPI document of its own, which is unusual and
|
|
5
|
+
* welcome, but ships it as a public preview: "subject to breaking changes
|
|
6
|
+
* without advance notice, and should not be used within production
|
|
7
|
+
* environments". Vendoring is what answers that. The committed copy never moves
|
|
8
|
+
* under us, so a breaking change upstream becomes a diff in a refresh commit
|
|
9
|
+
* rather than a provider that stops working — and the surface an operator's bot
|
|
10
|
+
* token can reach stays reviewable.
|
|
11
|
+
*
|
|
12
|
+
* bun run vendor:discord
|
|
13
|
+
*
|
|
14
|
+
* The pipeline is `src/providers/shared/vendor-spec.ts`. What stays here is what
|
|
15
|
+
* only Discord knows: which operations are worth exposing, and the three shapes
|
|
16
|
+
* in its document that the generator cannot take as written.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { vendorSpec } from '../../shared/vendor-spec.ts';
|
|
20
|
+
|
|
21
|
+
const SOURCE = 'https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The operations this provider exposes, out of 242 in the document.
|
|
25
|
+
*
|
|
26
|
+
* Curated hard, and the curation is load-bearing rather than tidy: `connect`
|
|
27
|
+
* writes one policy rule, `discord.*`, so an operation vendored here is an
|
|
28
|
+
* operation an agent may call. This list *is* the boundary. Discord's own API
|
|
29
|
+
* would otherwise hand over kick, ban, timeout, role assignment, channel
|
|
30
|
+
* deletion, and guild settings against a server the operator owns.
|
|
31
|
+
*
|
|
32
|
+
* What is deliberately absent, so a later refresh does not quietly add it back:
|
|
33
|
+
* `bulk_delete_messages` (a mass action no announcement workflow needs), every
|
|
34
|
+
* `deprecated_*` pin variant (superseded paths, kept by Discord for old
|
|
35
|
+
* clients), and everything under members, roles, bans, invites, and guild
|
|
36
|
+
* settings.
|
|
37
|
+
*/
|
|
38
|
+
const READS = [
|
|
39
|
+
// Whose token this is. The first call worth making after connecting, and the
|
|
40
|
+
// one that proves the `Bot ` prefix survived being pasted.
|
|
41
|
+
'get_my_user',
|
|
42
|
+
// Navigation: which servers the bot was added to, what is in them, and what
|
|
43
|
+
// one channel is. An agent cannot post to a channel it cannot name.
|
|
44
|
+
'list_my_guilds',
|
|
45
|
+
'get_guild',
|
|
46
|
+
'list_guild_channels',
|
|
47
|
+
'get_channel',
|
|
48
|
+
// The triage read. Discord exposes no message search to bots, so reading a
|
|
49
|
+
// channel means paging `list_messages` with `after` and `limit`.
|
|
50
|
+
'list_messages',
|
|
51
|
+
'get_message',
|
|
52
|
+
'list_pins',
|
|
53
|
+
'list_message_reactions_by_emoji',
|
|
54
|
+
'get_active_guild_threads',
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
const WRITES = [
|
|
58
|
+
// Posting, as the app.
|
|
59
|
+
'create_message',
|
|
60
|
+
// Editing and retracting one's own announcement. `update_message` is the
|
|
61
|
+
// typo fix; `delete_message` is the retraction, which is why it is here and
|
|
62
|
+
// `bulk_delete_messages` is not.
|
|
63
|
+
'update_message',
|
|
64
|
+
'delete_message',
|
|
65
|
+
// Publishing a message posted in an announcement channel out to the servers
|
|
66
|
+
// that follow it — the one operation that makes an announcement channel worth
|
|
67
|
+
// using rather than an ordinary one.
|
|
68
|
+
'crosspost_message',
|
|
69
|
+
// Triage marking: react to it, pin it, or turn it into a thread. All three
|
|
70
|
+
// are how a human reads back what the agent decided.
|
|
71
|
+
'add_my_message_reaction',
|
|
72
|
+
'create_pin',
|
|
73
|
+
'create_thread_from_message',
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Posting under the operator's own name rather than the app's.
|
|
78
|
+
*
|
|
79
|
+
* Discord has no API for acting as a user account — automating one violates
|
|
80
|
+
* their terms — so an integration posts as an application, and an application's
|
|
81
|
+
* messages carry an `APP` badge that cannot be removed. What *can* be set is the
|
|
82
|
+
* name and avatar, and `execute_webhook` sets them per message. So an
|
|
83
|
+
* announcement can read as the operator while an ordinary reply reads as the
|
|
84
|
+
* integration, from one bot token.
|
|
85
|
+
*
|
|
86
|
+
* The cost is stated in `docs/detailed/security.md`: `create_webhook` and
|
|
87
|
+
* `list_channel_webhooks` return the webhook's token in their response, and a
|
|
88
|
+
* webhook token is a standalone credential for posting to that channel.
|
|
89
|
+
*/
|
|
90
|
+
const WEBHOOKS = ['list_channel_webhooks', 'create_webhook', 'execute_webhook'] as const;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Discord's v2 message components, collapsed to open objects.
|
|
94
|
+
*
|
|
95
|
+
* `mcp-from-openapi` inlines `$ref`s, and this union is referenced by four
|
|
96
|
+
* request schemas and again from inside two of its own members — so inlining
|
|
97
|
+
* duplicates the whole tree six times over. There is no wrapper schema to
|
|
98
|
+
* collapse instead; the union is written out inline at each site.
|
|
99
|
+
*
|
|
100
|
+
* Measured, this is the difference between a provider that ships and one that
|
|
101
|
+
* cannot: `execute_webhook` generates a 148.9 KB input schema against a 64 KB
|
|
102
|
+
* per-tool budget, `create_message` 76.2 KB, `update_message` 72.6 KB, and the
|
|
103
|
+
* whole surface 305.4 KB against a 192 KB budget. Opaque brings the worst tool to
|
|
104
|
+
* 10.4 KB and the surface to 34.1 KB.
|
|
105
|
+
*
|
|
106
|
+
* `RichEmbed` is deliberately *not* here. It costs 3.1 KB — 1.6% of the surface
|
|
107
|
+
* budget — and it is how an announcement gets a title, a colour, and fields, so
|
|
108
|
+
* it is the one rich shape worth schematising. Collapsing it instead was
|
|
109
|
+
* measured and moves the worst tool from 148.9 KB to 144.2 KB, which is to say
|
|
110
|
+
* embeds were never the problem.
|
|
111
|
+
*/
|
|
112
|
+
const OPAQUE = [
|
|
113
|
+
'ActionRowComponentForMessageRequest',
|
|
114
|
+
'ContainerComponentForMessageRequest',
|
|
115
|
+
'FileComponentForMessageRequest',
|
|
116
|
+
'MediaGalleryComponentForMessageRequest',
|
|
117
|
+
'SectionComponentForMessageRequest',
|
|
118
|
+
'SeparatorComponentForMessageRequest',
|
|
119
|
+
'TextDisplayComponentForMessageRequest',
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
await vendorSpec('discord', {
|
|
123
|
+
source: SOURCE,
|
|
124
|
+
outputDirectory: import.meta.dir,
|
|
125
|
+
out: 'discord.v10.json',
|
|
126
|
+
operations: [...READS, ...WRITES, ...WEBHOOKS],
|
|
127
|
+
opaque: OPAQUE,
|
|
128
|
+
opaqueNote:
|
|
129
|
+
'Structure omitted: this union inlines to hundreds of kilobytes. ' +
|
|
130
|
+
'Pass the object as documented at https://discord.com/developers/docs/components/reference.',
|
|
131
|
+
vendoredNote:
|
|
132
|
+
'Trimmed by src/providers/discord/specs/vendor.ts. Committed deliberately: upstream is a public preview that may change without notice, and a spec decides which paths are called with the operator bot token — so it must be reviewable rather than fetched at connect time.',
|
|
133
|
+
|
|
134
|
+
// Discord declares path parameters on the path item, not the operation. That
|
|
135
|
+
// is legal, and `mcp-from-openapi`'s validator does not read it: without this
|
|
136
|
+
// the document fails outright with 29 × MISSING_PATH_PARAMETER and generates
|
|
137
|
+
// no tools at all.
|
|
138
|
+
hoistPathParameters: true,
|
|
139
|
+
|
|
140
|
+
// `create_message`, `update_message` and `execute_webhook` also offer
|
|
141
|
+
// `multipart/form-data`, whose file fields are named `files[0]`. That is not a
|
|
142
|
+
// legal tool property name, and one illegal name rejects the entire tools list
|
|
143
|
+
// for every provider on the endpoint — so the only thing standing between us
|
|
144
|
+
// and that is the generator preferring JSON of its own accord.
|
|
145
|
+
//
|
|
146
|
+
// The transport can now form-encode as well as JSON-encode (ADR-045), but not
|
|
147
|
+
// multipart, so a multipart branch is unsendable regardless of which one the
|
|
148
|
+
// generator picks. Attachments are unavailable as a result, which is the honest
|
|
149
|
+
// state: ADR-017's machinery is the route to them, not a branch nothing sends.
|
|
150
|
+
requestContentTypes: ['application/json'],
|
|
151
|
+
|
|
152
|
+
// `execute_webhook`'s body is a top-level `anyOf` of two complete payload
|
|
153
|
+
// schemas. A union has no `properties` to walk, so the generator emits one
|
|
154
|
+
// argument literally named `body` and the connector then sends
|
|
155
|
+
// `{"body":{"content":"…"}}` where Discord expects `{"content":"…"}`. Nothing
|
|
156
|
+
// in the test suite can see this: the name is legal, the size is fine, the
|
|
157
|
+
// base_url matches — only a live call fails.
|
|
158
|
+
//
|
|
159
|
+
// Naming the branch that is actually wanted flattens it back to 17 real
|
|
160
|
+
// arguments. Lossless: the other branch's properties are a strict subset.
|
|
161
|
+
rewriteRequestBody: {
|
|
162
|
+
execute_webhook: { $ref: '#/components/schemas/IncomingWebhookRequestPartial' },
|
|
163
|
+
},
|
|
164
|
+
});
|
|
@@ -15,23 +15,13 @@
|
|
|
15
15
|
* command rather than a hand edit.
|
|
16
16
|
*
|
|
17
17
|
* bun run vendor:google
|
|
18
|
+
*
|
|
19
|
+
* The pipeline itself is `src/providers/shared/vendor-spec.ts`. What stays here
|
|
20
|
+
* is what only Google knows: which operations are worth exposing, and which of
|
|
21
|
+
* its system parameters have to go.
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
|
-
import {
|
|
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
|
-
}
|
|
24
|
+
import { vendorSpec } from '../../shared/vendor-spec.ts';
|
|
35
25
|
|
|
36
26
|
/**
|
|
37
27
|
* The operations each provider exposes.
|
|
@@ -321,126 +311,6 @@ const SELECTION: Record<
|
|
|
321
311
|
// field but `title` on it anyway.
|
|
322
312
|
};
|
|
323
313
|
|
|
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
314
|
/**
|
|
445
315
|
* Google's system parameters, dropped from every operation.
|
|
446
316
|
*
|
|
@@ -474,188 +344,33 @@ const SYSTEM_PARAMETERS = new Set([
|
|
|
474
344
|
]);
|
|
475
345
|
|
|
476
346
|
/**
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
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.
|
|
347
|
+
* The note recorded in each spec's `info`, and the one `makeOpaque` leaves
|
|
348
|
+
* behind. Both are baked into the committed JSON, so they are passed verbatim
|
|
349
|
+
* rather than reworded.
|
|
483
350
|
*/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
351
|
+
const VENDORED_NOTE =
|
|
352
|
+
'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.';
|
|
353
|
+
|
|
354
|
+
const OPAQUE_NOTE =
|
|
355
|
+
'Structure omitted: it expands to megabytes when inlined. ' +
|
|
356
|
+
'Pass the object as documented at https://developers.google.com/workspace.';
|
|
357
|
+
|
|
358
|
+
for (const [id, selection] of Object.entries(SELECTION)) {
|
|
359
|
+
await vendorSpec(id, {
|
|
360
|
+
source: selection.source,
|
|
361
|
+
// `import.meta.dir` *is* the specs directory — this script lives beside what
|
|
362
|
+
// it writes. It used to be `scripts/vendor-google-specs.ts` and reached
|
|
363
|
+
// across the tree; commit 3cd03ce moved the script and the specs together
|
|
364
|
+
// but not the path arithmetic, so for a while this resolved to
|
|
365
|
+
// `src/providers/google/providers/builtin/specs`, created it, reported
|
|
366
|
+
// success, and left the committed spec untouched — making the documented
|
|
367
|
+
// "re-run the script" step a silent no-op.
|
|
368
|
+
outputDirectory: import.meta.dir,
|
|
369
|
+
out: selection.out,
|
|
370
|
+
operations: selection.operations,
|
|
371
|
+
...(selection.opaque ? { opaque: selection.opaque } : {}),
|
|
372
|
+
opaqueNote: OPAQUE_NOTE,
|
|
373
|
+
vendoredNote: VENDORED_NOTE,
|
|
374
|
+
systemParameters: SYSTEM_PARAMETERS,
|
|
503
375
|
});
|
|
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
376
|
}
|
|
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);
|
package/src/providers/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ProviderDefinition, ProviderManifest } from '#connectivity';
|
|
2
|
+
import { bunq } from './bunq/index.ts';
|
|
2
3
|
import { calendar } from './google/calendar/index.ts';
|
|
4
|
+
import { discord } from './discord/index.ts';
|
|
3
5
|
import { contacts } from './google/contacts/index.ts';
|
|
4
6
|
import { docs } from './google/docs/index.ts';
|
|
5
7
|
import { github } from './github/index.ts';
|
|
@@ -16,6 +18,7 @@ import { icloudDrive } from './icloud/drive/index.ts';
|
|
|
16
18
|
import { icloudMail } from './icloud/mail/index.ts';
|
|
17
19
|
import { linear } from './linear/index.ts';
|
|
18
20
|
import { notion } from './notion/index.ts';
|
|
21
|
+
import { reddit } from './reddit/index.ts';
|
|
19
22
|
import { slack } from './slack/index.ts';
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -53,6 +56,8 @@ export const PROVIDERS: readonly (ProviderManifest | ProviderDefinition)[] = [
|
|
|
53
56
|
linear,
|
|
54
57
|
github,
|
|
55
58
|
slack,
|
|
59
|
+
reddit,
|
|
60
|
+
discord,
|
|
56
61
|
gmail,
|
|
57
62
|
drive,
|
|
58
63
|
sheets,
|
|
@@ -67,6 +72,7 @@ export const PROVIDERS: readonly (ProviderManifest | ProviderDefinition)[] = [
|
|
|
67
72
|
icloudCalendar,
|
|
68
73
|
icloudContacts,
|
|
69
74
|
icloudDrive,
|
|
75
|
+
bunq,
|
|
70
76
|
];
|
|
71
77
|
|
|
72
78
|
/** The manifest half of an entry, whichever shape it arrived in. */
|
|
@@ -95,8 +101,11 @@ export {
|
|
|
95
101
|
tasks,
|
|
96
102
|
} from './google/index.ts';
|
|
97
103
|
export { icloudCalendar, icloudContacts, icloudDrive, icloudMail } from './icloud/index.ts';
|
|
104
|
+
export { bunq } from './bunq/index.ts';
|
|
105
|
+
export { discord } from './discord/index.ts';
|
|
98
106
|
export { github } from './github/index.ts';
|
|
99
107
|
export { linear } from './linear/index.ts';
|
|
100
108
|
export { notion } from './notion/index.ts';
|
|
109
|
+
export { reddit } from './reddit/index.ts';
|
|
101
110
|
export { slack } from './slack/index.ts';
|
|
102
111
|
export { SCOPE_MEANINGS, type ScopeMeaning } from './scopes.ts';
|