@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,245 @@
|
|
|
1
|
+
import { RESERVED_PROVIDER_IDS } from '#connectivity';
|
|
2
|
+
import type { MergedCapability } from './visibility.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the endpoint says about itself, in the `initialize` response.
|
|
6
|
+
*
|
|
7
|
+
* A client that has just connected holds sixty tools with good individual
|
|
8
|
+
* descriptions and no account of what the thing *is* — that there is memory
|
|
9
|
+
* worth consulting before answering from nothing, that a skill is the owner's
|
|
10
|
+
* own procedure rather than a suggestion, that `profile` is how someone keeps
|
|
11
|
+
* work and personal apart and is therefore never a field to guess at. Every one
|
|
12
|
+
* of those is a habit, and a habit does not fit in a tool description.
|
|
13
|
+
*
|
|
14
|
+
* MCP has one field for this and we were not using it. It is the only channel
|
|
15
|
+
* that reaches a client with no skills directory and no config we may write —
|
|
16
|
+
* so it carries the part that must arrive everywhere, and the bundled skill
|
|
17
|
+
* carries the longer form for the two harnesses that can hold one.
|
|
18
|
+
*
|
|
19
|
+
* **Generated, not written.** The prose below is fixed; the facts under it are
|
|
20
|
+
* this principal's, computed from the same policy-filtered set the tools were
|
|
21
|
+
* registered from. It is rebuilt per request over HTTP, so it cannot describe a
|
|
22
|
+
* profile that has gone away or omit one that was added.
|
|
23
|
+
*
|
|
24
|
+
* Two constraints on editing it:
|
|
25
|
+
*
|
|
26
|
+
* - **Length is a recurring cost.** This lands in the system prompt of every
|
|
27
|
+
* session, so a paragraph added here is paid for on every request forever.
|
|
28
|
+
* `instructions.test.ts` holds a budget.
|
|
29
|
+
* - **No vendor may be named.** `src/architecture.test.ts` forbids it anywhere
|
|
30
|
+
* under `server/`, and rightly: the list below is whatever the owner has
|
|
31
|
+
* connected, and prose that named one would be wrong for everybody else.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The habits, in the order they are needed.
|
|
36
|
+
*
|
|
37
|
+
* Routing first because it gates every call; refusals last because that is when
|
|
38
|
+
* an agent is most tempted to improvise. Second person, and specific about what
|
|
39
|
+
* *not* to do — "ask which profile" is advice, "do not default to the first" is
|
|
40
|
+
* a rule.
|
|
41
|
+
*
|
|
42
|
+
* **Four of these are conditional**, and that is a correctness property rather
|
|
43
|
+
* than a saving. This used to be one fixed string that told every client to
|
|
44
|
+
* consult memory, invoke skills, and guard vault values — on an endpoint
|
|
45
|
+
* granting none of the three, which is the common case for a workspace that
|
|
46
|
+
* connected a mailbox and nothing else. Prose describing tools that are not
|
|
47
|
+
* there is worse than absent prose: it is a promise the tool list contradicts,
|
|
48
|
+
* and an agent resolves that by guessing. The saving is real too, and it is
|
|
49
|
+
* what pays for `SETUP` fitting inside the budget below.
|
|
50
|
+
*
|
|
51
|
+
* The opening no longer enumerates what is here for the same reason. The
|
|
52
|
+
* listing at the end says what is reachable, computed rather than asserted.
|
|
53
|
+
*/
|
|
54
|
+
const OPENING = `This endpoint is one place to reach what its owner has chosen to expose. It
|
|
55
|
+
authenticates, applies permissions, and records what happened, so you do not
|
|
56
|
+
have to.`;
|
|
57
|
+
|
|
58
|
+
const ROUTING = `**Routing.** Every tool takes \`profile\` and \`connection\`. A profile is how
|
|
59
|
+
someone separates work from personal — when it is ambiguous which one is meant,
|
|
60
|
+
ask. Do not default to whichever is listed first.`;
|
|
61
|
+
|
|
62
|
+
const MEMORY = `**Memory is worth consulting.** Before concluding you do not know something
|
|
63
|
+
about this person or their work, search it. Writing to memory is a separate
|
|
64
|
+
grant, and what you write is served back to every later session — including to
|
|
65
|
+
a different agent — so write when asked to remember something, not by habit.`;
|
|
66
|
+
|
|
67
|
+
const SKILLS = `**Skills are the owner's procedures**, surfaced as prompts rather than tools.
|
|
68
|
+
That is deliberate: a procedure is selected by the person, not chosen by the
|
|
69
|
+
model, and you cannot read one's body. They belong to one profile, so a skill
|
|
70
|
+
you saw under one is not available under another. If a task has a skill for it,
|
|
71
|
+
say so and let them invoke it rather than improvising your own version.`;
|
|
72
|
+
|
|
73
|
+
const VAULT = `**Vault values are credentials.** Use one to do the thing that needs it. Do not
|
|
74
|
+
quote it back, summarise it, or write it anywhere.`;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The one that exists because its absence was observed, not predicted.
|
|
78
|
+
*
|
|
79
|
+
* Asked to connect a second mailbox, a client with no setup surface and no
|
|
80
|
+
* skill answered that it could not and then invented the procedure — edit the
|
|
81
|
+
* profile YAML, run "the auth command" — neither of which is a thing. It had
|
|
82
|
+
* no way to know `setup_overview` answers exactly that, so the instruction has
|
|
83
|
+
* to arrive here: this is the only channel that reaches a client which has
|
|
84
|
+
* merely been pointed at the URL.
|
|
85
|
+
*/
|
|
86
|
+
const SETUP = `**What is set up is answerable.** Before saying something cannot be reached, or
|
|
87
|
+
that an account must be added, call \`setup_overview\` — then \`setup_provider\`
|
|
88
|
+
for the exact command. Running it is the owner's to do; inventing it is not.`;
|
|
89
|
+
|
|
90
|
+
const FILES = `**Files are named, not carried.** Where a tool takes attachments, give a path, an
|
|
91
|
+
HTTPS URL, or an attachment already on another message; the endpoint reads the
|
|
92
|
+
bytes. Never encode a file into a call — that is the thing this replaces.`;
|
|
93
|
+
|
|
94
|
+
const REFUSAL = `**A refused call is the permission system working**, not an obstacle to route
|
|
95
|
+
around. Report what was refused and let the owner decide whether to widen it.
|
|
96
|
+
Every call, including a refused one, is recorded.`;
|
|
97
|
+
|
|
98
|
+
/** Which paragraph each owner-layer provider brings, when it is reachable. */
|
|
99
|
+
const OWNER_HABITS: Record<string, string> = {
|
|
100
|
+
memory: MEMORY,
|
|
101
|
+
skills: SKILLS,
|
|
102
|
+
vault: VAULT,
|
|
103
|
+
setup: SETUP,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The whole string's ceiling, and the only budget there is.
|
|
108
|
+
*
|
|
109
|
+
* This lands in the system prompt of every session against this endpoint, so a
|
|
110
|
+
* paragraph added here is paid for on every request forever. Needing to raise
|
|
111
|
+
* it is the prompt to ask whether the paragraph belongs in the skill instead,
|
|
112
|
+
* where it is loaded only when relevant.
|
|
113
|
+
*
|
|
114
|
+
* Exported because the test asserted `2000` as a literal while the code
|
|
115
|
+
* reserved room against a second, differently-derived number — so the two could
|
|
116
|
+
* disagree, and did. There is no separate listing allowance any more: `spent`
|
|
117
|
+
* measures the prose that was actually assembled, and `spent + form.length` is
|
|
118
|
+
* exactly the final length, because `join` adds the same two characters the
|
|
119
|
+
* reduce already counted.
|
|
120
|
+
*/
|
|
121
|
+
export const MAX_INSTRUCTIONS = 2000;
|
|
122
|
+
|
|
123
|
+
/** Which of the owner-layer providers this principal can actually reach. */
|
|
124
|
+
function ownerProviders(merged: ReadonlyMap<string, MergedCapability>): string[] {
|
|
125
|
+
const present = new Set<string>();
|
|
126
|
+
|
|
127
|
+
for (const id of merged.keys()) {
|
|
128
|
+
const provider = id.slice(0, id.indexOf('.'));
|
|
129
|
+
if (RESERVED_PROVIDER_IDS.includes(provider)) present.add(provider);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return RESERVED_PROVIDER_IDS.filter((id) => present.has(id));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The connections each profile contributes, deduplicated.
|
|
137
|
+
*
|
|
138
|
+
* Taken from `merged` rather than from each profile's config, so it lists what
|
|
139
|
+
* is *reachable* rather than what is configured. A connection the principal has
|
|
140
|
+
* no grant for is not registered on any tool, and announcing it here would
|
|
141
|
+
* describe a door that does not open.
|
|
142
|
+
*/
|
|
143
|
+
function connectionsByProfile(
|
|
144
|
+
profiles: readonly string[],
|
|
145
|
+
merged: ReadonlyMap<string, MergedCapability>,
|
|
146
|
+
): Map<string, string[]> {
|
|
147
|
+
const found = new Map<string, Set<string>>();
|
|
148
|
+
|
|
149
|
+
for (const entry of merged.values()) {
|
|
150
|
+
for (const [profile, connections] of entry.reachable) {
|
|
151
|
+
const known = found.get(profile) ?? new Set<string>();
|
|
152
|
+
for (const connection of connections) known.add(connection);
|
|
153
|
+
found.set(profile, known);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Ordered by the served list rather than by discovery, so the listing is
|
|
158
|
+
// stable between requests and reads the same as everywhere else.
|
|
159
|
+
const listed = new Map<string, string[]>();
|
|
160
|
+
for (const profile of profiles) {
|
|
161
|
+
const known = found.get(profile);
|
|
162
|
+
if (known) listed.set(profile, [...known].sort());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return listed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Profile *names*, not runtimes: this needs the order they are served in and
|
|
170
|
+
* nothing else, and a signature that asked for more would imply it reads more.
|
|
171
|
+
*/
|
|
172
|
+
export function serverInstructions(
|
|
173
|
+
profiles: readonly string[],
|
|
174
|
+
merged: ReadonlyMap<string, MergedCapability>,
|
|
175
|
+
): string {
|
|
176
|
+
const reachable = connectionsByProfile(profiles, merged);
|
|
177
|
+
const owner = ownerProviders(merged);
|
|
178
|
+
|
|
179
|
+
// Assembled per principal, because the owner layer is granted per principal.
|
|
180
|
+
// `ownerProviders` is already ordered by `RESERVED_PROVIDER_IDS`, so the
|
|
181
|
+
// paragraphs keep one order between requests rather than discovery order.
|
|
182
|
+
const sections = [
|
|
183
|
+
OPENING,
|
|
184
|
+
ROUTING,
|
|
185
|
+
...owner.map((id) => OWNER_HABITS[id]).filter((habit): habit is string => habit !== undefined),
|
|
186
|
+
FILES,
|
|
187
|
+
REFUSAL,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (reachable.size === 0) {
|
|
191
|
+
// Not an error state worth hiding: a workspace with no connection yet, or a
|
|
192
|
+
// principal granted nothing, both land here, and saying so beats a heading
|
|
193
|
+
// with nothing under it.
|
|
194
|
+
sections.push(
|
|
195
|
+
'Nothing is reachable through this endpoint yet — no connection is both configured and permitted.',
|
|
196
|
+
);
|
|
197
|
+
return sections.join('\n\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const lines = [...reachable].map(
|
|
201
|
+
([profile, connections]) => ` ${profile}: ${connections.join(', ')}`,
|
|
202
|
+
);
|
|
203
|
+
const listing = `Reachable now, by profile:\n${lines.join('\n')}`;
|
|
204
|
+
|
|
205
|
+
// The prose above varies per principal; this listing grows with the workspace.
|
|
206
|
+
// Either can be the half that does not fit, so all three widths are measured
|
|
207
|
+
// against the one ceiling rather than against a reserve guessed in advance —
|
|
208
|
+
// which is how a workspace of one profile and one mailbox ended up being told
|
|
209
|
+
// "1 profiles" with a hundred characters of the budget unspent.
|
|
210
|
+
//
|
|
211
|
+
// Summarising rather than truncating, and safe to do: every tool carries the
|
|
212
|
+
// connections it accepts in its own `connection` enum, which is the
|
|
213
|
+
// authoritative list. This paragraph is orientation, so a count and the profile
|
|
214
|
+
// names lose nothing an agent cannot get exactly.
|
|
215
|
+
const total = [...reachable.values()].reduce((sum, list) => sum + list.length, 0);
|
|
216
|
+
const names = [...reachable.keys()];
|
|
217
|
+
const tail = "Each tool's `connection` argument lists the ones it accepts.";
|
|
218
|
+
const plural = names.length === 1 ? 'profile' : 'profiles';
|
|
219
|
+
|
|
220
|
+
// Widest first. The last is bounded — it names no profile — which is what
|
|
221
|
+
// makes the ceiling hold for a workspace of any size.
|
|
222
|
+
const forms = [
|
|
223
|
+
listing,
|
|
224
|
+
`Reachable now: ${total} connections across ${names.join(', ')}. ${tail}`,
|
|
225
|
+
`Reachable now: ${total} connections across ${names.length} ${plural}. ${tail}`,
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const spent = sections.reduce((count, section) => count + section.length + 2, 0);
|
|
229
|
+
|
|
230
|
+
// Every candidate is checked, including the last: it is shorter than naming
|
|
231
|
+
// twenty profiles but not shorter than naming one, so choosing it unmeasured
|
|
232
|
+
// both overran the budget in one direction and wasted it in the other. If
|
|
233
|
+
// nothing fits, the shortest is the most honest thing left to say.
|
|
234
|
+
sections.push(
|
|
235
|
+
forms.find((form) => spent + form.length < MAX_INSTRUCTIONS) ??
|
|
236
|
+
forms.reduce((shortest, form) => (form.length < shortest.length ? form : shortest)),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// No trailing "the owner's own material is here too, under: …" line any more.
|
|
240
|
+
// Each of those providers now brings its own paragraph when it is reachable,
|
|
241
|
+
// so the list repeated what the prose had just said — and it swept `setup`
|
|
242
|
+
// in with memory, skills and vault, which it is not: it holds none of the
|
|
243
|
+
// owner's material and only describes what the others are.
|
|
244
|
+
return sections.join('\n\n');
|
|
245
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability ids on the wire.
|
|
3
|
+
*
|
|
4
|
+
* MCP tool names are restricted to `[A-Za-z0-9_-]`, so the dotted capability id
|
|
5
|
+
* becomes `gmail_search` on the wire. The dotted form stays canonical
|
|
6
|
+
* everywhere it matters — config, policy rules, audit records — because that is
|
|
7
|
+
* what an operator reads and writes. Only the wire name is transliterated, and
|
|
8
|
+
* the mapping is total and reversible because provider ids and capability names
|
|
9
|
+
* both exclude `-`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const SERVER_NAME = 'lanes-link';
|
|
13
|
+
|
|
14
|
+
export function toolNameFor(capabilityId: string): string {
|
|
15
|
+
return capabilityId.replace(/\./g, '_');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recover the capability id a wire name came from.
|
|
20
|
+
*
|
|
21
|
+
* Splitting on the first `_` was reversible while a capability name was one
|
|
22
|
+
* segment. It stopped being so when OpenAPI operationIds arrived dotted:
|
|
23
|
+
* `gmail_users_drafts_send` splits to `gmail.users_drafts_send`, which names
|
|
24
|
+
* nothing. That only shows up in the audit record for a refused tool — a log
|
|
25
|
+
* entry saying an agent tried something that does not exist, spelled wrongly,
|
|
26
|
+
* is worse than useless.
|
|
27
|
+
*
|
|
28
|
+
* So the known ids are consulted first, and the split is the fallback for a
|
|
29
|
+
* name matching no capability at all — where an approximate spelling is the
|
|
30
|
+
* best available and the attempt is what matters.
|
|
31
|
+
*/
|
|
32
|
+
export function capabilityIdForToolName(toolName: string, known?: Iterable<string>): string {
|
|
33
|
+
for (const id of known ?? []) {
|
|
34
|
+
if (toolNameFor(id) === toolName) return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const index = toolName.indexOf('_');
|
|
38
|
+
return index === -1 ? toolName : `${toolName.slice(0, index)}.${toolName.slice(index + 1)}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { isPromptResult } from '#connectivity';
|
|
4
|
+
import { toolNameFor } from './naming.ts';
|
|
5
|
+
import { resolveScope } from './routing.ts';
|
|
6
|
+
import { describeWithConnections, type BuildServerOptions, type MergedCapability } from './visibility.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prompts — the owner's own procedures, surfaced as slash commands.
|
|
10
|
+
*
|
|
11
|
+
* Unlike a resource, a prompt *does* have arguments, so routing goes in them —
|
|
12
|
+
* ADR-001 rather than ADR-006's URI workaround. Both are optional and default
|
|
13
|
+
* when there is only one candidate, because a prompt's arguments are typically
|
|
14
|
+
* filled in by a person choosing a slash command, and making them type two
|
|
15
|
+
* routing strings to reach their only account would be a poor trade for
|
|
16
|
+
* consistency.
|
|
17
|
+
*/
|
|
18
|
+
export function registerPrompt(
|
|
19
|
+
server: McpServer,
|
|
20
|
+
id: string,
|
|
21
|
+
entry: MergedCapability,
|
|
22
|
+
capability: Extract<NonNullable<MergedCapability['capability']>, { kind: 'prompt' }>,
|
|
23
|
+
options: BuildServerOptions,
|
|
24
|
+
): void {
|
|
25
|
+
const profiles = [...entry.reachable.keys()];
|
|
26
|
+
const connections = [...new Set([...entry.reachable.values()].flat())];
|
|
27
|
+
|
|
28
|
+
const shape: Record<string, z.ZodType> = {};
|
|
29
|
+
|
|
30
|
+
for (const argument of capability.arguments ?? []) {
|
|
31
|
+
const declared = z.string().describe(argument.description);
|
|
32
|
+
shape[argument.name] = argument.required ? declared : declared.optional();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
shape['profile'] = z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(`Which profile to act within: ${profiles.join(', ')}. Omit if there is one.`);
|
|
39
|
+
shape['connection'] = z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(`Which configured account: ${connections.join(', ')}. Omit if there is one.`);
|
|
43
|
+
|
|
44
|
+
server.registerPrompt(
|
|
45
|
+
toolNameFor(id),
|
|
46
|
+
{
|
|
47
|
+
...(capability.title ? { title: capability.title } : {}),
|
|
48
|
+
description: describeWithConnections(capability.description, entry.reachable),
|
|
49
|
+
argsSchema: z.object(shape),
|
|
50
|
+
},
|
|
51
|
+
async (args: Record<string, unknown>) => {
|
|
52
|
+
const { profile, connection, ...rest } = args;
|
|
53
|
+
const scope = resolveScope(entry, profile, connection);
|
|
54
|
+
if ('error' in scope) throw new Error(scope.error);
|
|
55
|
+
|
|
56
|
+
const outcome = await options.profiles.get(scope.profile)!.dispatcher.invoke({
|
|
57
|
+
principal: options.principal,
|
|
58
|
+
capabilityId: id,
|
|
59
|
+
connectionKey: scope.connectionKey,
|
|
60
|
+
arguments: rest,
|
|
61
|
+
clientLabel: options.clientLabel,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// A prompt has no `isError` to carry a refusal in, so a denial is a
|
|
65
|
+
// protocol error. That is the honest mapping: there is no partial prompt
|
|
66
|
+
// to hand back, and the message still says why.
|
|
67
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
68
|
+
if (!isPromptResult(outcome.result)) throw new Error(`${id} did not return a prompt`);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
messages: outcome.result.messages.map((message) => ({
|
|
72
|
+
role: message.role,
|
|
73
|
+
content: { type: 'text' as const, text: message.text },
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ResourceTemplate, type McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { isResourceListResult, isResourceResult } from '#connectivity';
|
|
3
|
+
import type { DispatchOutcome } from '#dispatch';
|
|
4
|
+
import { toolNameFor } from './naming.ts';
|
|
5
|
+
import { scopeResourceUri } from './routing.ts';
|
|
6
|
+
import type { BuildServerOptions, MergedCapability } from './visibility.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resources — read-oriented context, addressed rather than called.
|
|
10
|
+
*
|
|
11
|
+
* One registration per (profile, connection): a resource URI carries no
|
|
12
|
+
* argument to route on, so both have to live in the URI itself — ADR-006. A
|
|
13
|
+
* read still goes through the dispatcher like everything else, so a resource is
|
|
14
|
+
* policy-checked, rate-limited, and audited on exactly the terms a tool is.
|
|
15
|
+
*/
|
|
16
|
+
export function registerResource(
|
|
17
|
+
server: McpServer,
|
|
18
|
+
id: string,
|
|
19
|
+
entry: MergedCapability,
|
|
20
|
+
capability: Extract<NonNullable<MergedCapability['capability']>, { kind: 'resource' }>,
|
|
21
|
+
options: BuildServerOptions,
|
|
22
|
+
): void {
|
|
23
|
+
for (const [profile, reachable] of entry.reachable) {
|
|
24
|
+
const runtime = options.profiles.get(profile)!;
|
|
25
|
+
|
|
26
|
+
for (const connectionKey of reachable) {
|
|
27
|
+
const connectionId = connectionKey.slice(connectionKey.indexOf('.') + 1);
|
|
28
|
+
const scope = { profile, connectionId };
|
|
29
|
+
const scoped = scopeResourceUri(capability.uriTemplate, scope);
|
|
30
|
+
|
|
31
|
+
const dispatch = (args: Record<string, unknown>): Promise<DispatchOutcome> =>
|
|
32
|
+
runtime.dispatcher.invoke({
|
|
33
|
+
principal: options.principal,
|
|
34
|
+
capabilityId: id,
|
|
35
|
+
connectionKey,
|
|
36
|
+
arguments: args,
|
|
37
|
+
clientLabel: options.clientLabel,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const metadata = {
|
|
41
|
+
description: `${capability.description} (${profile}: ${connectionKey})`,
|
|
42
|
+
...(capability.mimeType ? { mimeType: capability.mimeType } : {}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// `uri` is the whole argument — the provider recovers its own template
|
|
46
|
+
// variables from it and never sees the routing segments core prepended.
|
|
47
|
+
const read = async (uri: URL) => {
|
|
48
|
+
const outcome = await dispatch({ uri: uri.href });
|
|
49
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
50
|
+
if (!isResourceResult(outcome.result)) {
|
|
51
|
+
throw new Error(`${id} did not return resource contents`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
contents: outcome.result.contents.map((part) => ({
|
|
56
|
+
// The requested URI, not whatever the provider echoed: MCP requires
|
|
57
|
+
// them to match, and a provider returning its own unscoped form
|
|
58
|
+
// would produce contents a client cannot re-read.
|
|
59
|
+
uri: uri.href,
|
|
60
|
+
...(part.mimeType ?? capability.mimeType
|
|
61
|
+
? { mimeType: part.mimeType ?? capability.mimeType! }
|
|
62
|
+
: {}),
|
|
63
|
+
text: part.text,
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const name = `${toolNameFor(id)}_${profile}_${connectionId}`;
|
|
69
|
+
|
|
70
|
+
// A template with nothing to expand is a static resource, and the SDK
|
|
71
|
+
// wants it registered as one — that is what puts it in `resources/list`
|
|
72
|
+
// without a list callback.
|
|
73
|
+
if (!scoped.includes('{')) {
|
|
74
|
+
server.registerResource(name, scoped, metadata, read);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
server.registerResource(
|
|
79
|
+
name,
|
|
80
|
+
new ResourceTemplate(scoped, {
|
|
81
|
+
// `undefined` is a deliberate value here, not an omission: the SDK
|
|
82
|
+
// requires the key so that forgetting to enumerate is a decision
|
|
83
|
+
// rather than an oversight. A provider omits `list` when its resource
|
|
84
|
+
// space is unbounded.
|
|
85
|
+
list: capability.list
|
|
86
|
+
? async () => {
|
|
87
|
+
const outcome = await dispatch({});
|
|
88
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
89
|
+
if (!isResourceListResult(outcome.result)) return { resources: [] };
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
resources: outcome.result.resources.map((resource) => ({
|
|
93
|
+
name: resource.name,
|
|
94
|
+
uri: scopeResourceUri(resource.uri, scope),
|
|
95
|
+
...(capability.mimeType ? { mimeType: capability.mimeType } : {}),
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
}),
|
|
101
|
+
metadata,
|
|
102
|
+
read,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isResource } from '#connectivity';
|
|
2
|
+
import type { MergedCapability, ProfileRuntime } from './visibility.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Getting the profile and the connection into an address.
|
|
6
|
+
*
|
|
7
|
+
* A tool takes them as injected arguments (ADR-001). A resource has no argument
|
|
8
|
+
* to route on, so they go into the URI; a prompt has arguments but is typically
|
|
9
|
+
* filled in by a person choosing a slash command, so they are optional there and
|
|
10
|
+
* default when there is only one candidate.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Put the profile and the connection into a resource URI.
|
|
15
|
+
*
|
|
16
|
+
* They have to go somewhere that works for *any* template, not just one that
|
|
17
|
+
* happens to spell `{key}`, so they are inserted as the first two path segments
|
|
18
|
+
* directly after the authority: `example://note/{key}` becomes
|
|
19
|
+
* `example://note/personal/a/{key}`, and `memory://entry/{id}` becomes
|
|
20
|
+
* `memory://entry/personal/owner/{id}`.
|
|
21
|
+
*
|
|
22
|
+
* The previous form substituted the literal token `{key}`, which meant any
|
|
23
|
+
* provider naming its variable anything else — every provider except `example` —
|
|
24
|
+
* registered a URI with no routing in it at all, and two connections would have
|
|
25
|
+
* collided on one address.
|
|
26
|
+
*/
|
|
27
|
+
export function scopeResourceUri(
|
|
28
|
+
uriOrTemplate: string,
|
|
29
|
+
scope: { profile: string; connectionId: string },
|
|
30
|
+
): string {
|
|
31
|
+
const separator = uriOrTemplate.indexOf('://');
|
|
32
|
+
if (separator === -1) return uriOrTemplate;
|
|
33
|
+
|
|
34
|
+
const scheme = uriOrTemplate.slice(0, separator + 3);
|
|
35
|
+
const rest = uriOrTemplate.slice(separator + 3);
|
|
36
|
+
const slash = rest.indexOf('/');
|
|
37
|
+
const authority = slash === -1 ? rest : rest.slice(0, slash);
|
|
38
|
+
const path = slash === -1 ? '' : rest.slice(slash);
|
|
39
|
+
|
|
40
|
+
return `${scheme}${authority}/${scope.profile}/${scope.connectionId}${path}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `scheme://authority`, or empty for anything not shaped like that. */
|
|
44
|
+
function originOf(uri: string): string {
|
|
45
|
+
const separator = uri.indexOf('://');
|
|
46
|
+
if (separator === -1) return '';
|
|
47
|
+
|
|
48
|
+
const rest = uri.slice(separator + 3);
|
|
49
|
+
const slash = rest.indexOf('/');
|
|
50
|
+
return uri.slice(0, separator + 3) + (slash === -1 ? rest : rest.slice(0, slash));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Route the `resource_link`s a tool hands back.
|
|
55
|
+
*
|
|
56
|
+
* A provider names its own resources — `memory://entry/deploy_window` — because
|
|
57
|
+
* it does not know, and must not learn, which profile or connection it is
|
|
58
|
+
* serving. Core does. Without this, `memory.search` returns addresses that look
|
|
59
|
+
* like resources and cannot be read, which is worse than returning none.
|
|
60
|
+
*
|
|
61
|
+
* Scoped **only** for a link whose scheme and authority match one of this
|
|
62
|
+
* provider's own resource templates. A link to somewhere else — an `https://`
|
|
63
|
+
* document a vendor returned — is left exactly as the vendor wrote it; inserting
|
|
64
|
+
* routing segments into someone else's URL would corrupt it.
|
|
65
|
+
*/
|
|
66
|
+
export function resourceLinkRouter(
|
|
67
|
+
runtime: ProfileRuntime,
|
|
68
|
+
capabilityId: string,
|
|
69
|
+
profile: string,
|
|
70
|
+
connectionKey: string,
|
|
71
|
+
): (uri: string) => string {
|
|
72
|
+
const providerId = capabilityId.slice(0, capabilityId.indexOf('.'));
|
|
73
|
+
const definition = runtime.registry.get(providerId)?.definition;
|
|
74
|
+
if (!definition) return (uri) => uri;
|
|
75
|
+
|
|
76
|
+
const origins = new Set(
|
|
77
|
+
definition.capabilities
|
|
78
|
+
.filter(isResource)
|
|
79
|
+
.map((capability) => originOf(capability.uriTemplate)),
|
|
80
|
+
);
|
|
81
|
+
if (origins.size === 0) return (uri) => uri;
|
|
82
|
+
|
|
83
|
+
const connectionId = connectionKey.slice(connectionKey.indexOf('.') + 1);
|
|
84
|
+
|
|
85
|
+
return (uri) =>
|
|
86
|
+
origins.has(originOf(uri)) ? scopeResourceUri(uri, { profile, connectionId }) : uri;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Which profile and connection a prompt call meant.
|
|
91
|
+
*
|
|
92
|
+
* Same refusal a tool gets when a profile and a connection belong to different
|
|
93
|
+
* profiles, for the same reason: the enums are a union, and routing a `work`
|
|
94
|
+
* account through `personal` would cross exactly the boundary profiles exist to
|
|
95
|
+
* hold.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveScope(
|
|
98
|
+
entry: MergedCapability,
|
|
99
|
+
profile: unknown,
|
|
100
|
+
connection: unknown,
|
|
101
|
+
): { profile: string; connectionKey: string } | { error: string } {
|
|
102
|
+
const profiles = [...entry.reachable.keys()];
|
|
103
|
+
const name = typeof profile === 'string' ? profile : (profiles.length === 1 ? profiles[0]! : '');
|
|
104
|
+
|
|
105
|
+
const reachable = entry.reachable.get(name);
|
|
106
|
+
if (!reachable) {
|
|
107
|
+
return { error: `Name a profile: ${profiles.join(', ')}` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const key =
|
|
111
|
+
typeof connection === 'string' ? connection : (reachable.length === 1 ? reachable[0]! : '');
|
|
112
|
+
if (!reachable.includes(key)) {
|
|
113
|
+
return { error: `Name a connection within profile "${name}": ${reachable.join(', ')}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { profile: name, connectionKey: key };
|
|
117
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making a discovered schema safe to publish.
|
|
3
|
+
*
|
|
4
|
+
* Two problems, both arriving from vendors rather than from us, and both fixed
|
|
5
|
+
* here rather than per-provider so an upstream MCP server and a hand-written
|
|
6
|
+
* workspace manifest get the same treatment as Google's specs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Property names a tool schema may use.
|
|
11
|
+
*
|
|
12
|
+
* The Anthropic API enforces `^[a-zA-Z0-9_.-]{1,64}$` on every property key,
|
|
13
|
+
* and rejects the **entire** `tools` array when one fails — so a single bad key
|
|
14
|
+
* anywhere takes down every provider on the endpoint, not just its own. Google's
|
|
15
|
+
* specs ship `$.xgafv`, which is exactly that.
|
|
16
|
+
*/
|
|
17
|
+
const LEGAL_PROPERTY = /^[a-zA-Z0-9_.-]{1,64}$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JSON Schema's own `format` values.
|
|
21
|
+
*
|
|
22
|
+
* Anything else is an OpenAPI or vendor annotation — `int64`, `uint64`,
|
|
23
|
+
* `float`, `byte`, Google's `google` — and a validator that does not recognise
|
|
24
|
+
* it logs a warning for every occurrence, every time a schema is compiled.
|
|
25
|
+
* Google's specs carry six such formats, which is where "unknown format uint64
|
|
26
|
+
* ignored" comes from.
|
|
27
|
+
*/
|
|
28
|
+
const STANDARD_FORMATS = new Set([
|
|
29
|
+
'date-time', 'date', 'time', 'duration',
|
|
30
|
+
'email', 'idn-email', 'hostname', 'idn-hostname',
|
|
31
|
+
'ipv4', 'ipv6', 'uri', 'uri-reference', 'uri-template',
|
|
32
|
+
'iri', 'iri-reference', 'uuid', 'regex',
|
|
33
|
+
'json-pointer', 'relative-json-pointer',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* **Illegal property names are dropped.** Dropping rather than renaming: the
|
|
38
|
+
* name is what the connector maps back to a request parameter, so a renamed key
|
|
39
|
+
* would arrive upstream as something the vendor does not recognise. A *required*
|
|
40
|
+
* property is left in place, to fail loudly rather than register a tool that can
|
|
41
|
+
* never be called correctly.
|
|
42
|
+
*
|
|
43
|
+
* **Non-standard `format` values are dropped.** They are annotations a JSON
|
|
44
|
+
* Schema validator has no rule for; it ignores them and says so, once per
|
|
45
|
+
* occurrence per compile. Removing them loses nothing — `type` still carries the
|
|
46
|
+
* constraint that matters — and removes a stream of warnings that buries
|
|
47
|
+
* anything worth reading.
|
|
48
|
+
*/
|
|
49
|
+
export function sanitizeSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
50
|
+
const walk = (node: unknown): unknown => {
|
|
51
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
52
|
+
if (node === null || typeof node !== 'object') return node;
|
|
53
|
+
|
|
54
|
+
const record = node as Record<string, unknown>;
|
|
55
|
+
const out: Record<string, unknown> = {};
|
|
56
|
+
|
|
57
|
+
const required = new Set((record['required'] as string[] | undefined) ?? []);
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(record)) {
|
|
60
|
+
if (key === 'format' && typeof value === 'string' && !STANDARD_FORMATS.has(value)) continue;
|
|
61
|
+
|
|
62
|
+
if (key === 'properties' && value !== null && typeof value === 'object') {
|
|
63
|
+
out[key] = Object.fromEntries(
|
|
64
|
+
Object.entries(value as Record<string, unknown>)
|
|
65
|
+
.filter(([name]) => LEGAL_PROPERTY.test(name) || required.has(name))
|
|
66
|
+
.map(([name, child]) => [name, walk(child)]),
|
|
67
|
+
);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
out[key] = walk(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return walk(schema) as Record<string, unknown>;
|
|
78
|
+
}
|