@lanes-sh/link 0.6.10 → 0.7.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/.gcloudignore +25 -15
- package/README.md +62 -45
- package/instructions/skills/lanes-link/SKILL.md +25 -0
- package/package.json +3 -2
- package/src/audit/conformance.ts +1 -1
- package/src/cli/argv.ts +14 -1
- package/src/cli/commands/connect/acquire.ts +100 -0
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/declare.ts +19 -0
- package/src/cli/commands/connect/index.ts +54 -56
- package/src/cli/commands/connect/requirements.ts +23 -1
- package/src/cli/commands/connect/setup.ts +1 -1
- package/src/cli/commands/connect/variables.ts +220 -0
- package/src/cli/commands/knowledge/index.ts +22 -4
- package/src/cli/commands/knowledge/migrate.ts +61 -43
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/owner/entities.ts +244 -0
- package/src/cli/commands/owner/shared.ts +14 -0
- package/src/cli/commands/owner/vault.ts +2 -2
- package/src/cli/commands/owner.ts +13 -3
- package/src/cli/commands/setup.ts +12 -0
- package/src/cli/config-edit.ts +16 -6
- package/src/cli/config-repair.ts +10 -0
- package/src/cli/dispatch-owner.ts +30 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/runtime/open.ts +14 -7
- package/src/cli/runtime/registry.ts +6 -1
- package/src/cli/selection.ts +11 -0
- package/src/cli/usage.ts +13 -2
- package/src/connectivity/auth/strategy/index.ts +1 -1
- package/src/connectivity/capability.ts +1 -1
- package/src/connectivity/context.ts +1 -1
- package/src/connectivity/index.ts +1 -1
- package/src/connectivity/mail/attachments.ts +42 -73
- package/src/connectivity/mail/content-type.ts +71 -0
- package/src/connectivity/mail/index.ts +1 -1
- package/src/connectivity/manifest/provider.ts +50 -0
- package/src/connectivity/manifest/variables.ts +173 -0
- package/src/connectivity/transports/factory.ts +74 -1
- package/src/connectivity/transports/http/index.ts +8 -2
- package/src/connectivity/transports/http/keys.ts +83 -0
- package/src/connectivity/transports/imap/attachment.ts +53 -10
- package/src/connectivity/transports/imap/capabilities.ts +28 -0
- package/src/connectivity/transports/imap/download.ts +111 -0
- package/src/connectivity/transports/imap/index.ts +13 -0
- package/src/connectivity/transports/imap/operations.ts +1 -0
- package/src/deployments/adapters/s3.ts +1 -1
- package/src/deployments/gcp/provision.ts +1 -1
- package/src/deployments/knowledge.ts +44 -18
- package/src/deployments/prepare.ts +1 -1
- package/src/deployments/serving.ts +1 -1
- package/src/dispatch/dispatch.ts +17 -10
- package/src/dispatch/staging.ts +76 -1
- package/src/profile/knowledge.ts +19 -11
- package/src/profile/schema.ts +2 -2
- package/src/providers/README.md +189 -0
- package/src/providers/airtable/index.ts +10 -0
- package/src/providers/algolia/index.ts +10 -0
- package/src/providers/amplitude/index.ts +10 -0
- package/src/providers/apify/index.ts +10 -0
- package/src/providers/asana/index.ts +10 -0
- package/src/providers/assets/provider.ts +11 -4
- package/src/providers/atlassian/index.ts +19 -0
- package/src/providers/attio/index.ts +10 -0
- package/src/providers/betterstack/index.ts +10 -0
- package/src/providers/box/index.ts +64 -0
- package/src/providers/brightdata/index.ts +10 -0
- package/src/providers/buildkite/index.ts +10 -0
- package/src/providers/bunq/index.ts +2 -2
- package/src/providers/calendly/index.ts +10 -0
- package/src/providers/canva/index.ts +10 -0
- package/src/providers/circleci/index.ts +10 -0
- package/src/providers/clickup/index.ts +10 -0
- package/src/providers/close/index.ts +10 -0
- package/src/providers/cloudflare_bindings/index.ts +14 -0
- package/src/providers/cloudflare_observability/index.ts +10 -0
- package/src/providers/contentful/index.ts +10 -0
- package/src/providers/custom/load.ts +1 -1
- package/src/providers/datadog/index.ts +10 -0
- package/src/providers/discord/index.ts +1 -1
- package/src/providers/discord/specs/discord.v10.json +0 -88
- package/src/providers/discord/specs/vendor.ts +1 -1
- package/src/providers/dropbox/index.ts +10 -0
- package/src/providers/entities/catalogue.ts +366 -0
- package/src/providers/entities/find.ts +279 -0
- package/src/providers/entities/provider.ts +334 -0
- package/src/providers/entities/render.ts +142 -0
- package/src/providers/entities/store.ts +276 -0
- package/src/providers/entities/writes.ts +279 -0
- package/src/providers/example/provider.ts +1 -1
- package/src/providers/expensify/index.ts +10 -0
- package/src/providers/fastmail/calendar/index.ts +21 -0
- package/src/providers/fastmail/contacts/index.ts +17 -0
- package/src/providers/fastmail/index.ts +4 -0
- package/src/providers/fastmail/mail/index.ts +38 -0
- package/src/providers/fastmail/shared/setup.ts +52 -0
- package/src/providers/figma/index.ts +10 -0
- package/src/providers/fireflies/index.ts +10 -0
- package/src/providers/flagsmith/index.ts +10 -0
- package/src/providers/gamma/index.ts +10 -0
- package/src/providers/github/index.ts +2 -2
- package/src/providers/google/gmail/attachment.ts +16 -1
- package/src/providers/google/gmail-imap/index.ts +7 -2
- package/src/providers/google/shared/oauth.ts +1 -1
- package/src/providers/google/shared/service-account.ts +1 -1
- package/src/providers/google/shared/setup.ts +1 -1
- package/src/providers/google/specs/calendar.v3.json +0 -32
- package/src/providers/google/specs/docs.v1.json +0 -76
- package/src/providers/google/specs/drive.v3.json +0 -32
- package/src/providers/google/specs/gmail.v1.json +0 -76
- package/src/providers/google/specs/people.v1.json +0 -77
- package/src/providers/google/specs/sheets.v4.json +0 -76
- package/src/providers/google/specs/tasks.v1.json +0 -76
- package/src/providers/grafana/index.ts +10 -0
- package/src/providers/heroku/index.ts +10 -0
- package/src/providers/hubspot/index.ts +75 -0
- package/src/providers/hygraph/index.ts +10 -0
- package/src/providers/icloud/mail/index.ts +5 -0
- package/src/providers/icloud/shared/setup.ts +1 -1
- package/src/providers/index.ts +255 -0
- package/src/providers/insightly/index.ts +10 -0
- package/src/providers/jam/index.ts +10 -0
- package/src/providers/klaviyo/index.ts +10 -0
- package/src/providers/mailbox/index.ts +87 -0
- package/src/providers/memory/provider.ts +3 -8
- package/src/providers/mercury/index.ts +10 -0
- package/src/providers/microsoft/calendar/index.ts +49 -0
- package/src/providers/microsoft/contacts/index.ts +50 -0
- package/src/providers/microsoft/drive/index.ts +62 -0
- package/src/providers/microsoft/index.ts +12 -0
- package/src/providers/microsoft/mail/index.ts +76 -0
- package/src/providers/microsoft/shared/oauth.ts +58 -0
- package/src/providers/microsoft/shared/scopes.ts +56 -0
- package/src/providers/microsoft/shared/setup.ts +45 -0
- package/src/providers/microsoft/specs/microsoft-todo.v1.json +755 -0
- package/src/providers/microsoft/specs/onedrive.v1.json +993 -0
- package/src/providers/microsoft/specs/outlook-calendar.v1.json +1743 -0
- package/src/providers/microsoft/specs/outlook-contacts.v1.json +430 -0
- package/src/providers/microsoft/specs/outlook-mail.v1.json +901 -0
- package/src/providers/microsoft/specs/vendor.ts +232 -0
- package/src/providers/microsoft/todo/index.ts +54 -0
- package/src/providers/miro/index.ts +10 -0
- package/src/providers/mixpanel/index.ts +10 -0
- package/src/providers/monday/index.ts +10 -0
- package/src/providers/mux/index.ts +10 -0
- package/src/providers/navan/index.ts +10 -0
- package/src/providers/neon/index.ts +10 -0
- package/src/providers/netlify/index.ts +10 -0
- package/src/providers/nextcloud/calendar/index.ts +28 -0
- package/src/providers/nextcloud/contacts/index.ts +18 -0
- package/src/providers/nextcloud/index.ts +3 -0
- package/src/providers/nextcloud/shared/setup.ts +59 -0
- package/src/providers/owner.ts +15 -3
- package/src/providers/paddle/index.ts +10 -0
- package/src/providers/paypal/index.ts +10 -0
- package/src/providers/posthog/index.ts +10 -0
- package/src/providers/prisma/index.ts +10 -0
- package/src/providers/ramp/index.ts +10 -0
- package/src/providers/recurly/index.ts +10 -0
- package/src/providers/reddit/index.ts +1 -1
- package/src/providers/remote/index.ts +10 -0
- package/src/providers/render/index.ts +35 -0
- package/src/providers/replicate/index.ts +10 -0
- package/src/providers/resend/index.ts +10 -0
- package/src/providers/riverside/index.ts +10 -0
- package/src/providers/rootly/index.ts +10 -0
- package/src/providers/rudderstack/index.ts +10 -0
- package/src/providers/salesloft/index.ts +10 -0
- package/src/providers/sanity/index.ts +10 -0
- package/src/providers/scopes.ts +2 -0
- package/src/providers/sentry/index.ts +10 -0
- package/src/providers/setup/plan.ts +11 -0
- package/src/providers/setup/provider.ts +12 -0
- package/src/providers/shared/openapi.ts +70 -0
- package/src/providers/shared/slug.ts +28 -0
- package/src/providers/shared/vendor-operations.ts +43 -1
- package/src/providers/shared/vendor-spec.ts +98 -17
- package/src/providers/shortcut/index.ts +10 -0
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/square/index.ts +10 -0
- package/src/providers/storyblok/index.ts +10 -0
- package/src/providers/stripe/index.ts +10 -0
- package/src/providers/supabase/index.ts +10 -0
- package/src/providers/tasks/store.ts +2 -7
- package/src/providers/tavily/index.ts +10 -0
- package/src/providers/todoist/index.ts +10 -0
- package/src/providers/untested.ts +105 -0
- package/src/providers/vault/provider.ts +1 -1
- package/src/providers/vercel/index.ts +10 -0
- package/src/providers/vimeo/index.ts +10 -0
- package/src/providers/webflow/index.ts +10 -0
- package/src/providers/whimsical/index.ts +10 -0
- package/src/providers/wix/index.ts +10 -0
- package/src/providers/workable/index.ts +10 -0
- package/src/providers/yahoo_mail/index.ts +73 -0
- package/src/providers/zapier/index.ts +10 -0
- package/src/providers/zoho_mail/index.ts +79 -0
- package/src/secrets/document.ts +1 -1
- package/src/server/attachments.ts +129 -5
- package/src/server/index.ts +3 -3
- package/src/server/mcp/instructions.ts +88 -18
- package/src/server/stdio.ts +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Replicate registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const replicate = defineProvider({
|
|
5
|
+
id: 'replicate',
|
|
6
|
+
name: 'Replicate',
|
|
7
|
+
description: 'Models, predictions, and deployments, via Replicate\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.replicate.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Resend registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const resend = defineProvider({
|
|
5
|
+
id: 'resend',
|
|
6
|
+
name: 'Resend',
|
|
7
|
+
description: 'Transactional email, domains, and delivery events, via Resend\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.resend.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Riverside registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const riverside = defineProvider({
|
|
5
|
+
id: 'riverside',
|
|
6
|
+
name: 'Riverside',
|
|
7
|
+
description: 'Recordings, transcripts, and clips, via Riverside\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.riverside.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Rootly registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const rootly = defineProvider({
|
|
5
|
+
id: 'rootly',
|
|
6
|
+
name: 'Rootly',
|
|
7
|
+
description: 'Incidents, alerts, retrospectives, and on-call schedules, via Rootly\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.rootly.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** RudderStack registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const rudderstack = defineProvider({
|
|
5
|
+
id: 'rudderstack',
|
|
6
|
+
name: 'RudderStack',
|
|
7
|
+
description: 'Sources, destinations, and event streams, via RudderStack\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.rudderstack.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Salesloft registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const salesloft = defineProvider({
|
|
5
|
+
id: 'salesloft',
|
|
6
|
+
name: 'Salesloft',
|
|
7
|
+
description: 'Cadences, people, and sales activity, via Salesloft\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.salesloft.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Sanity registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const sanity = defineProvider({
|
|
5
|
+
id: 'sanity',
|
|
6
|
+
name: 'Sanity',
|
|
7
|
+
description: 'Documents, datasets, schema, and content releases, via Sanity\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.sanity.io/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
package/src/providers/scopes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GOOGLE_SCOPE_MEANINGS } from './google/shared/scopes.ts';
|
|
2
2
|
import { LINEAR_SCOPE_MEANINGS } from './linear/scopes.ts';
|
|
3
|
+
import { MICROSOFT_SCOPE_MEANINGS } from './microsoft/shared/scopes.ts';
|
|
3
4
|
import { REDDIT_SCOPE_MEANINGS } from './reddit/scopes.ts';
|
|
4
5
|
import { SLACK_SCOPE_MEANINGS } from './slack/scopes.ts';
|
|
5
6
|
|
|
@@ -25,6 +26,7 @@ export interface ScopeMeaning {
|
|
|
25
26
|
export const SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
26
27
|
...GOOGLE_SCOPE_MEANINGS,
|
|
27
28
|
...LINEAR_SCOPE_MEANINGS,
|
|
29
|
+
...MICROSOFT_SCOPE_MEANINGS,
|
|
28
30
|
...REDDIT_SCOPE_MEANINGS,
|
|
29
31
|
...SLACK_SCOPE_MEANINGS,
|
|
30
32
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Sentry registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const sentry = defineProvider({
|
|
5
|
+
id: 'sentry',
|
|
6
|
+
name: 'Sentry',
|
|
7
|
+
description: 'Issues, events, stack traces, and releases, via Sentry\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.sentry.dev/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -16,6 +16,13 @@ import { hasOwnClientPath, RESERVED_PROVIDER_IDS, setupRequirements, type SetupR
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
export interface ProviderPlan {
|
|
19
|
+
/** Where the service is, for a provider whose address the connection supplies. */
|
|
20
|
+
readonly variables: readonly {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
readonly example: string;
|
|
25
|
+
}[];
|
|
19
26
|
readonly id: string;
|
|
20
27
|
readonly name: string;
|
|
21
28
|
readonly description: string;
|
|
@@ -145,6 +152,10 @@ export function planFor(
|
|
|
145
152
|
...(manifest.setup?.docs_url ? { docsUrl: manifest.setup.docs_url } : {}),
|
|
146
153
|
steps: manifest.setup?.steps ?? [],
|
|
147
154
|
requires: requirements,
|
|
155
|
+
// Not a `requires` entry, and the difference is the point: a variable is not
|
|
156
|
+
// a secret, has no ref, and no `secrets set` line would place it. It is
|
|
157
|
+
// asked for at connect and written to the connection's own row.
|
|
158
|
+
variables: manifest.variables,
|
|
148
159
|
needsId,
|
|
149
160
|
command,
|
|
150
161
|
brokered,
|
|
@@ -305,6 +305,18 @@ function renderProvider(plan: ProviderPlan): string {
|
|
|
305
305
|
for (const requirement of plan.requires) lines.push(` ${requirement.label}`);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Listed alongside the credentials, because to the person answering they are
|
|
309
|
+
// the same thing — another question `connect` will ask. Omitted here would be
|
|
310
|
+
// worse than for a credential: an agent relaying a setup plan for Nextcloud
|
|
311
|
+
// would say "your username and password" and never mention the one value that
|
|
312
|
+
// provider cannot be connected without.
|
|
313
|
+
if (plan.variables.length > 0) {
|
|
314
|
+
lines.push('', 'And where the service is:');
|
|
315
|
+
for (const variable of plan.variables) {
|
|
316
|
+
lines.push(` ${variable.label} — ${variable.description} For example: ${variable.example}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
308
320
|
if (plan.needsId) {
|
|
309
321
|
lines.push(
|
|
310
322
|
'',
|
|
@@ -62,6 +62,76 @@ export function referenced(root: unknown, schemas: Record<string, unknown>): Set
|
|
|
62
62
|
return found;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Every component the document actually reaches, section by section.
|
|
67
|
+
*
|
|
68
|
+
* `referenced` above answers the same question for `schemas` alone, which was
|
|
69
|
+
* enough while every vendored document kept its other component sections small.
|
|
70
|
+
* Microsoft Graph does not: its published OpenAPI carries 726 shared responses
|
|
71
|
+
* and 1,419 examples, and copying those through untouched left a seven-operation
|
|
72
|
+
* spec at 962 KB with **733 dangling references** — because the responses that
|
|
73
|
+
* survived pointed at schemas the trim had just removed. The generator answered
|
|
74
|
+
* that with `Invalid OpenAPI document` and no tools at all.
|
|
75
|
+
*
|
|
76
|
+
* So reachability has to cross sections rather than stop at schemas: a path
|
|
77
|
+
* reaches a response, which reaches a schema, which reaches another. One walk,
|
|
78
|
+
* following `#/components/<section>/<name>` wherever it points, and whatever is
|
|
79
|
+
* not reached is not carried.
|
|
80
|
+
*
|
|
81
|
+
* `securitySchemes` is the exception and is never returned here — it is named by
|
|
82
|
+
* the `security` array rather than by `$ref`, so a reachability walk cannot see
|
|
83
|
+
* it and its caller keeps it whole.
|
|
84
|
+
*/
|
|
85
|
+
export function reachableComponents(
|
|
86
|
+
root: unknown,
|
|
87
|
+
components: Record<string, unknown>,
|
|
88
|
+
): Record<string, Set<string>> {
|
|
89
|
+
const found: Record<string, Set<string>> = {};
|
|
90
|
+
const queue: unknown[] = [root];
|
|
91
|
+
|
|
92
|
+
const resolve = (pointer: string): unknown => {
|
|
93
|
+
const rest = pointer.slice('#/components/'.length);
|
|
94
|
+
const slash = rest.indexOf('/');
|
|
95
|
+
if (slash < 0) return undefined;
|
|
96
|
+
|
|
97
|
+
const section = rest.slice(0, slash);
|
|
98
|
+
// JSON Pointer escaping. Component names rarely need it, and a name that
|
|
99
|
+
// does would otherwise be recorded under a spelling the filter never matches.
|
|
100
|
+
const name = rest.slice(slash + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
101
|
+
|
|
102
|
+
const entries = components[section];
|
|
103
|
+
if (entries === null || typeof entries !== 'object') return undefined;
|
|
104
|
+
if (!(name in (entries as Record<string, unknown>))) return undefined;
|
|
105
|
+
|
|
106
|
+
const seen = (found[section] ??= new Set());
|
|
107
|
+
if (seen.has(name)) return undefined;
|
|
108
|
+
seen.add(name);
|
|
109
|
+
|
|
110
|
+
return (entries as Record<string, unknown>)[name];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
while (queue.length > 0) {
|
|
114
|
+
const node = queue.pop();
|
|
115
|
+
if (node === null || typeof node !== 'object') continue;
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(node)) {
|
|
118
|
+
queue.push(...node);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [key, value] of Object.entries(node)) {
|
|
123
|
+
if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/')) {
|
|
124
|
+
const target = resolve(value);
|
|
125
|
+
if (target !== undefined) queue.push(target);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
queue.push(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
/**
|
|
66
136
|
* Cut reference cycles, replacing the back-edge with an open object.
|
|
67
137
|
*
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable id from something the owner typed, so writing does not demand one be
|
|
3
|
+
* invented.
|
|
4
|
+
*
|
|
5
|
+
* Three owner-layer stores derive an id this way — a memory entry from its
|
|
6
|
+
* title, a task from its title, an entity from its name — and they must derive
|
|
7
|
+
* it *identically*, because the id is the filename and a person who has learned
|
|
8
|
+
* what one store does with an apostrophe has learned what all three do.
|
|
9
|
+
*
|
|
10
|
+
* The 60-character cap is the whole of the length policy. It is not about any
|
|
11
|
+
* filesystem limit: it is that an id appears in a policy rule, a resource URI
|
|
12
|
+
* and an audit line, and a title-length one is unreadable in all three.
|
|
13
|
+
*
|
|
14
|
+
* `fallback` is the prefix for a name that slugifies to nothing — a title of
|
|
15
|
+
* only punctuation, or of a script this transliterates away. Producing
|
|
16
|
+
* `entry-14` rather than throwing is deliberate: the caller has content to
|
|
17
|
+
* store, and refusing it because its title is CJK would be a worse answer than
|
|
18
|
+
* an ugly id the owner can rename.
|
|
19
|
+
*/
|
|
20
|
+
export function slugify(text: string, fallback: string): string {
|
|
21
|
+
const slug = text
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '')
|
|
25
|
+
.slice(0, 60);
|
|
26
|
+
|
|
27
|
+
return slug.length > 0 ? slug : `${fallback}-${text.length}`;
|
|
28
|
+
}
|
|
@@ -127,6 +127,48 @@ export function narrowRequestBody(
|
|
|
127
127
|
* Apply before reachability, so a schema the projection no longer reaches leaves
|
|
128
128
|
* the document rather than lingering unused.
|
|
129
129
|
*/
|
|
130
|
+
/**
|
|
131
|
+
* Every property a schema describes, including the ones it inherits.
|
|
132
|
+
*
|
|
133
|
+
* OpenAPI spells inheritance `allOf: [{ $ref: parent }, { properties: … }]`, and
|
|
134
|
+
* a schema written that way has no `properties` of its own at the top level.
|
|
135
|
+
* Microsoft Graph uses it for every entity: `microsoft.graph.message` declares
|
|
136
|
+
* thirty-one properties and inherits `categories` from `outlookItem`, so reading
|
|
137
|
+
* `properties` off the schema alone reports a field the vendor plainly does
|
|
138
|
+
* describe as missing — and the refusal that exists to catch a rename fires on a
|
|
139
|
+
* document that never changed.
|
|
140
|
+
*
|
|
141
|
+
* Nearest definition wins, which is what `allOf` means: a later member overrides
|
|
142
|
+
* an earlier one, and the schema's own `properties` override everything it
|
|
143
|
+
* inherits. `seen` is against a cycle in the inheritance chain rather than in the
|
|
144
|
+
* data — `cutCycles` handles the other kind, and this walk would not reach it.
|
|
145
|
+
*/
|
|
146
|
+
function describedProperties(
|
|
147
|
+
schema: unknown,
|
|
148
|
+
schemas: Record<string, unknown>,
|
|
149
|
+
seen: Set<string> = new Set(),
|
|
150
|
+
): Record<string, unknown> {
|
|
151
|
+
if (schema === null || typeof schema !== 'object') return {};
|
|
152
|
+
const record = schema as Record<string, unknown>;
|
|
153
|
+
|
|
154
|
+
const reference = record['$ref'];
|
|
155
|
+
if (typeof reference === 'string') {
|
|
156
|
+
if (!reference.startsWith('#/components/schemas/')) return {};
|
|
157
|
+
const name = reference.slice('#/components/schemas/'.length);
|
|
158
|
+
if (seen.has(name)) return {};
|
|
159
|
+
seen.add(name);
|
|
160
|
+
return describedProperties(schemas[name], schemas, seen);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const collected: Record<string, unknown> = {};
|
|
164
|
+
for (const member of (record['allOf'] as unknown[] | undefined) ?? []) {
|
|
165
|
+
Object.assign(collected, describedProperties(member, schemas, seen));
|
|
166
|
+
}
|
|
167
|
+
Object.assign(collected, (record['properties'] as Record<string, unknown> | undefined) ?? {});
|
|
168
|
+
|
|
169
|
+
return collected;
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
export function projectRequestBody(
|
|
131
173
|
operation: Record<string, unknown>,
|
|
132
174
|
operationId: string,
|
|
@@ -154,7 +196,7 @@ export function projectRequestBody(
|
|
|
154
196
|
}
|
|
155
197
|
|
|
156
198
|
const name = reference.slice('#/components/schemas/'.length);
|
|
157
|
-
const source = (schemas[name]
|
|
199
|
+
const source = describedProperties(schemas[name], schemas);
|
|
158
200
|
|
|
159
201
|
const properties: Record<string, unknown> = {};
|
|
160
202
|
for (const field of fields) {
|
|
@@ -16,15 +16,16 @@
|
|
|
16
16
|
* provider that has that shape.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { OpenAPIToolGenerator, type McpOpenAPITool } from 'mcp-from-openapi';
|
|
22
|
-
import { cutCycles, makeOpaque,
|
|
22
|
+
import { cutCycles, makeOpaque, reachableComponents, type Spec } from './openapi.ts';
|
|
23
23
|
import {
|
|
24
24
|
METHODS,
|
|
25
25
|
dropSystemParameters,
|
|
26
26
|
hoistParameters,
|
|
27
27
|
narrowRequestBody,
|
|
28
|
+
projectRequestBody,
|
|
28
29
|
} from './vendor-operations.ts';
|
|
29
30
|
|
|
30
31
|
/** Kept in step with `BUDGET_KB` in `src/cli/tools.test.ts`, which enforces it. */
|
|
@@ -98,6 +99,23 @@ export interface VendorSpecOptions {
|
|
|
98
99
|
* the document rather than lingering unused.
|
|
99
100
|
*/
|
|
100
101
|
readonly rewriteRequestBody?: Readonly<Record<string, unknown>>;
|
|
102
|
+
/**
|
|
103
|
+
* Narrow an operation's request body to named fields, keeping the vendor's own
|
|
104
|
+
* types and descriptions.
|
|
105
|
+
*
|
|
106
|
+
* The remedy for a body that references a wide entity. Microsoft Graph's
|
|
107
|
+
* `PATCH /me/messages/{id}` takes a whole `microsoft.graph.message`, which
|
|
108
|
+
* inlines to 346 KB against a 64 KB budget — and almost all of it is fields
|
|
109
|
+
* the server computes and ignores on a write.
|
|
110
|
+
*
|
|
111
|
+
* Preferred over `rewriteRequestBody` wherever the body is a schema reference,
|
|
112
|
+
* because it is checked: a field the vendor has renamed, removed, or made
|
|
113
|
+
* read-only fails the refresh instead of becoming an argument the API quietly
|
|
114
|
+
* discards. Reach for the rewrite only where there is no schema to project.
|
|
115
|
+
*/
|
|
116
|
+
readonly projectRequestBody?: Readonly<Record<string, readonly string[]>>;
|
|
117
|
+
/** What a projected body says about itself. One sentence, baked into the spec. */
|
|
118
|
+
readonly projectionNote?: string;
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
export async function vendorSpec(id: string, options: VendorSpecOptions): Promise<void> {
|
|
@@ -161,6 +179,7 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
let narrowed = 0;
|
|
182
|
+
let projected = 0;
|
|
164
183
|
for (const item of Object.values(paths)) {
|
|
165
184
|
for (const [method, operation] of Object.entries(item)) {
|
|
166
185
|
if (!METHODS.includes(method)) continue;
|
|
@@ -174,13 +193,44 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
174
193
|
);
|
|
175
194
|
}
|
|
176
195
|
|
|
196
|
+
const fields = operation.operationId
|
|
197
|
+
? options.projectRequestBody?.[operation.operationId]
|
|
198
|
+
: undefined;
|
|
199
|
+
if (fields && operation.operationId) {
|
|
200
|
+
projectRequestBody(
|
|
201
|
+
holder,
|
|
202
|
+
operation.operationId,
|
|
203
|
+
(spec.components?.schemas ?? {}) as Record<string, unknown>,
|
|
204
|
+
fields,
|
|
205
|
+
options.projectionNote ??
|
|
206
|
+
'The fields this call actually writes. The entity it shares a schema with is far wider, ' +
|
|
207
|
+
'and the rest of it is computed by the service rather than set here.',
|
|
208
|
+
);
|
|
209
|
+
projected += 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
177
212
|
const replacement = operation.operationId
|
|
178
213
|
? options.rewriteRequestBody?.[operation.operationId]
|
|
179
214
|
: undefined;
|
|
180
215
|
if (replacement) {
|
|
181
|
-
const body = holder['requestBody'] as
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
const body = holder['requestBody'] as
|
|
217
|
+
| { $ref?: string; content?: Record<string, unknown> }
|
|
218
|
+
| undefined;
|
|
219
|
+
|
|
220
|
+
// A body declared as a shared `requestBodies` component has no `content`
|
|
221
|
+
// here to overwrite — Microsoft Graph's `sendMail` is one, and the
|
|
222
|
+
// rewrite silently did nothing, leaving the 346 KB entity it was meant
|
|
223
|
+
// to replace. Replacing the whole node is the only way in, and it also
|
|
224
|
+
// detaches the operation from a component the trim would otherwise carry.
|
|
225
|
+
if (!body || typeof body.$ref === 'string' || !body.content) {
|
|
226
|
+
holder['requestBody'] = {
|
|
227
|
+
required: true,
|
|
228
|
+
content: { 'application/json': { schema: replacement } },
|
|
229
|
+
};
|
|
230
|
+
} else {
|
|
231
|
+
for (const type of Object.keys(body.content)) {
|
|
232
|
+
(body.content[type] as Record<string, unknown>)['schema'] = replacement;
|
|
233
|
+
}
|
|
184
234
|
}
|
|
185
235
|
}
|
|
186
236
|
}
|
|
@@ -212,10 +262,33 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
212
262
|
|
|
213
263
|
const schemas = spec.components?.schemas ?? {};
|
|
214
264
|
const opaqued = makeOpaque(schemas, options.opaque ?? [], options.opaqueNote ?? '');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
265
|
+
|
|
266
|
+
// Reachability across *every* component section, not just schemas. A shared
|
|
267
|
+
// response that survives while the schema it points at is trimmed away is a
|
|
268
|
+
// dangling reference, and a document full of those is one the generator
|
|
269
|
+
// refuses outright — see `reachableComponents`.
|
|
270
|
+
const reach = reachableComponents(paths, (spec.components ?? {}) as Record<string, unknown>);
|
|
271
|
+
|
|
272
|
+
const carried: Record<string, unknown> = {};
|
|
273
|
+
for (const [section, entries] of Object.entries(spec.components ?? {})) {
|
|
274
|
+
if (entries === null || typeof entries !== 'object') continue;
|
|
275
|
+
|
|
276
|
+
// Named by the `security` array rather than by `$ref`, so no walk can see
|
|
277
|
+
// it and dropping it would leave operations declaring auth that resolves to
|
|
278
|
+
// nothing.
|
|
279
|
+
if (section === 'securitySchemes') {
|
|
280
|
+
carried[section] = entries;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const keep = reach[section];
|
|
285
|
+
if (!keep || keep.size === 0) continue;
|
|
286
|
+
carried[section] = Object.fromEntries(
|
|
287
|
+
Object.entries(entries as Record<string, unknown>).filter(([name]) => keep.has(name)),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const trimmedSchemas = (carried['schemas'] ?? {}) as Record<string, unknown>;
|
|
219
292
|
const cuts = cutCycles(trimmedSchemas);
|
|
220
293
|
|
|
221
294
|
const trimmed: Spec = {
|
|
@@ -227,19 +300,18 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
227
300
|
},
|
|
228
301
|
...(spec.servers ? { servers: spec.servers } : {}),
|
|
229
302
|
paths,
|
|
230
|
-
components:
|
|
303
|
+
components: carried,
|
|
231
304
|
};
|
|
232
305
|
|
|
233
306
|
await mkdir(options.outputDirectory, { recursive: true });
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
`${JSON.stringify(trimmed, null, 2)}\n`,
|
|
237
|
-
);
|
|
307
|
+
const outputPath = join(options.outputDirectory, options.out);
|
|
308
|
+
await writeFile(outputPath, `${JSON.stringify(trimmed, null, 2)}\n`);
|
|
238
309
|
|
|
239
310
|
const size = Math.round(JSON.stringify(trimmed).length / 1024);
|
|
240
311
|
const extra = [
|
|
241
312
|
options.hoistPathParameters ? `${hoisted} params hoisted` : '',
|
|
242
313
|
narrowed > 0 ? `${narrowed} body types dropped` : '',
|
|
314
|
+
projected > 0 ? `${projected} bodies projected` : '',
|
|
243
315
|
].filter(Boolean);
|
|
244
316
|
console.log(
|
|
245
317
|
` ${id.padEnd(6)} ${String(Object.keys(paths).length).padStart(2)} paths, ` +
|
|
@@ -249,7 +321,7 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
249
321
|
(extra.length > 0 ? `, ${extra.join(', ')}` : ''),
|
|
250
322
|
);
|
|
251
323
|
|
|
252
|
-
await reportLargestTools(id,
|
|
324
|
+
await reportLargestTools(id, outputPath, seen.size);
|
|
253
325
|
}
|
|
254
326
|
|
|
255
327
|
/**
|
|
@@ -265,17 +337,26 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
|
|
|
265
337
|
* Printed here so the refresh that adds an operation shows its cost, rather than
|
|
266
338
|
* leaving it to a test failure to say so after the fact.
|
|
267
339
|
*
|
|
340
|
+
* Measured by reading back the file that was just written, rather than the
|
|
341
|
+
* object it was written from. Those are not reliably the same document — a key
|
|
342
|
+
* holding `undefined` is present in memory and gone through `JSON.stringify` —
|
|
343
|
+
* and the validator refused an in-memory shape whose committed form it accepted,
|
|
344
|
+
* reporting "could not measure" against a spec that was in fact fine. The
|
|
345
|
+
* artifact is what the budget is about, so the artifact is what is measured.
|
|
346
|
+
*
|
|
268
347
|
* The count is the other half. The generator answers an unresolvable reference by
|
|
269
348
|
* logging to the console and omitting that one tool, so a document can trim
|
|
270
349
|
* cleanly, write successfully, and quietly advertise less than it selected.
|
|
271
350
|
* `tools.test.ts` only asserts the surface is non-empty, so nothing downstream
|
|
272
351
|
* would notice.
|
|
273
352
|
*/
|
|
274
|
-
async function reportLargestTools(id: string,
|
|
353
|
+
async function reportLargestTools(id: string, path: string, expected: number): Promise<void> {
|
|
275
354
|
let measured: Array<{ name: string; kb: number }>;
|
|
276
355
|
|
|
277
356
|
try {
|
|
278
|
-
const generator = await OpenAPIToolGenerator.fromJSON(
|
|
357
|
+
const generator = await OpenAPIToolGenerator.fromJSON(
|
|
358
|
+
JSON.parse(await readFile(path, 'utf8')) as Spec,
|
|
359
|
+
);
|
|
279
360
|
const tools = await generator.generateTools();
|
|
280
361
|
|
|
281
362
|
if (tools.length !== expected) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Shortcut registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const shortcut = defineProvider({
|
|
5
|
+
id: 'shortcut',
|
|
6
|
+
name: 'Shortcut',
|
|
7
|
+
description: 'Stories, epics, iterations, and workflows, via Shortcut\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.shortcut.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -98,11 +98,11 @@ export const slack = defineProvider({
|
|
|
98
98
|
'Slack normally needs nothing set up — one browser round trip against the app Lanes ' +
|
|
99
99
|
'registered. Pasting a token is the way past a workspace whose admin has not approved ' +
|
|
100
100
|
'that app, using one from an app the workspace already trusts.',
|
|
101
|
-
docs: 'docs/
|
|
101
|
+
docs: 'https://lanes.sh/docs/link/slack',
|
|
102
102
|
docs_url: 'https://api.slack.com/apps',
|
|
103
103
|
steps: [
|
|
104
104
|
'Open https://api.slack.com/apps and choose "Create New App" → "From scratch". Name it and pick the workspace.',
|
|
105
|
-
'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in docs/
|
|
105
|
+
'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in https://lanes.sh/docs/link/slack.',
|
|
106
106
|
'Choose "Install to Workspace" and approve. A Slack admin may have to approve it for you.',
|
|
107
107
|
'Copy the "User OAuth Token". It starts with xoxp- — not the bot token, which starts with xoxb- and will not work here.',
|
|
108
108
|
'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Square registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const square = defineProvider({
|
|
5
|
+
id: 'square',
|
|
6
|
+
name: 'Square',
|
|
7
|
+
description: 'Payments, orders, catalog, inventory, and customers, via Square\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.squareup.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Storyblok registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const storyblok = defineProvider({
|
|
5
|
+
id: 'storyblok',
|
|
6
|
+
name: 'Storyblok',
|
|
7
|
+
description: 'Stories, components, assets, and spaces, via Storyblok\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.storyblok.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Stripe registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const stripe = defineProvider({
|
|
5
|
+
id: 'stripe',
|
|
6
|
+
name: 'Stripe',
|
|
7
|
+
description: 'Payments, customers, invoices, subscriptions, and balances, via Stripe\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.stripe.com' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Supabase registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const supabase = defineProvider({
|
|
5
|
+
id: 'supabase',
|
|
6
|
+
name: 'Supabase',
|
|
7
|
+
description: 'Projects, database schema, SQL, edge functions, and docs, via Supabase\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.supabase.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
stringList,
|
|
5
5
|
withFrontmatter,
|
|
6
6
|
} from '#providers/shared/frontmatter.ts';
|
|
7
|
+
import { slugify as slugifyText } from '#providers/shared/slug.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* How a task is stored, and the only place that knows.
|
|
@@ -99,13 +100,7 @@ export function assertTaskId(id: string): void {
|
|
|
99
100
|
|
|
100
101
|
/** A stable id from a title, so adding a task does not demand one be invented. */
|
|
101
102
|
export function slugify(title: string): string {
|
|
102
|
-
|
|
103
|
-
.toLowerCase()
|
|
104
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
105
|
-
.replace(/^-+|-+$/g, '')
|
|
106
|
-
.slice(0, 60);
|
|
107
|
-
|
|
108
|
-
return slug.length > 0 ? slug : `task-${title.length}`;
|
|
103
|
+
return slugifyText(title, 'task');
|
|
109
104
|
}
|
|
110
105
|
|
|
111
106
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Tavily registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const tavily = defineProvider({
|
|
5
|
+
id: 'tavily',
|
|
6
|
+
name: 'Tavily',
|
|
7
|
+
description: 'Web search and page content extraction, via Tavily\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.tavily.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Todoist registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const todoist = defineProvider({
|
|
5
|
+
id: 'todoist',
|
|
6
|
+
name: 'Todoist',
|
|
7
|
+
description: 'Tasks, projects, sections, labels, and filters, via Todoist\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://ai.todoist.net/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|