@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,331 @@
|
|
|
1
|
+
import { challenge, type Authenticator } from '#auth';
|
|
2
|
+
import type { Logger } from '#connectivity';
|
|
3
|
+
import { capabilityIdForToolName } from '#server/mcp';
|
|
4
|
+
import { ATTACHMENTS_PATH, stageAttachment } from './attachments.ts';
|
|
5
|
+
import { allowedHostnamesFor, rebindingRefusal } from './rebinding.ts';
|
|
6
|
+
import type { Generation, Generations } from './generations.ts';
|
|
7
|
+
import { callerKey, failedAuthLimiter, FAILED_AUTH_PER_MINUTE, tooManyAttempts } from './edge.ts';
|
|
8
|
+
import {
|
|
9
|
+
handleAuthorization,
|
|
10
|
+
isAuthorizationPath,
|
|
11
|
+
resourceMetadataUrl,
|
|
12
|
+
type AuthorizationSurface,
|
|
13
|
+
} from './oauth.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The HTTP surface.
|
|
17
|
+
*
|
|
18
|
+
* Stateless streamable HTTP on a single endpoint. Statelessness is not an
|
|
19
|
+
* optimisation: the M2 target replaces instances between requests, so any
|
|
20
|
+
* in-memory session state would produce intermittent 404s. `createMcpHandler`
|
|
21
|
+
* builds a fresh server instance per request from the generation's factory,
|
|
22
|
+
* which is also what lets the tool list be a pure function of the caller's
|
|
23
|
+
* policy.
|
|
24
|
+
*
|
|
25
|
+
* Authentication happens here, in front of that factory, so the factory only
|
|
26
|
+
* ever runs for a caller whose identity is already established.
|
|
27
|
+
*
|
|
28
|
+
* What this file is *not* responsible for is which runtimes are current. A
|
|
29
|
+
* request pins one generation on the way in and uses it throughout, so a reload
|
|
30
|
+
* landing mid-request cannot change what that request is evaluated against.
|
|
31
|
+
* See `./generations.ts`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface ServerOptions {
|
|
35
|
+
/** The current profile runtimes, and the reload that replaces them. */
|
|
36
|
+
readonly generations: Generations;
|
|
37
|
+
/** Which profile's port, host, and token govern the endpoint itself. */
|
|
38
|
+
readonly primary: string;
|
|
39
|
+
readonly authenticator: Authenticator;
|
|
40
|
+
readonly log: Logger;
|
|
41
|
+
readonly version?: string;
|
|
42
|
+
/**
|
|
43
|
+
* How a remote client obtains a token, when the profile declares one.
|
|
44
|
+
*
|
|
45
|
+
* Absent means bearer-token-only: no metadata is published, the `401` carries
|
|
46
|
+
* no pointer, and the endpoint behaves exactly as it did before.
|
|
47
|
+
*/
|
|
48
|
+
readonly authorization?: AuthorizationSurface | undefined;
|
|
49
|
+
/** Hostnames this endpoint answers to. See `./rebinding.ts`. */
|
|
50
|
+
readonly allowedHostnames?: readonly string[] | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const MCP_PATH = '/mcp';
|
|
54
|
+
export const RELOAD_PATH = '/reload';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Loopback addresses. What binding to one changes is the browser threat model,
|
|
58
|
+
* not the auth model — see `allowedHostnames`.
|
|
59
|
+
*/
|
|
60
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
61
|
+
|
|
62
|
+
export function isLoopback(host: string): boolean {
|
|
63
|
+
return LOOPBACK.has(host);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How often an unrecognised tool name may provoke a reload.
|
|
68
|
+
*
|
|
69
|
+
* The safety net below reloads when a call names a tool this endpoint does not
|
|
70
|
+
* advertise, which is what a just-connected provider looks like to an instance
|
|
71
|
+
* that missed the notify. An agent retrying a genuinely absent tool must not
|
|
72
|
+
* turn that into a reload per call, so it is bounded.
|
|
73
|
+
*/
|
|
74
|
+
const RELOAD_PROBE_MS = 10_000;
|
|
75
|
+
|
|
76
|
+
export interface RequestHandler {
|
|
77
|
+
fetch(request: Request): Promise<Response>;
|
|
78
|
+
close(): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createRequestHandler(options: ServerOptions): RequestHandler {
|
|
82
|
+
let probedAt = 0;
|
|
83
|
+
const failedAuth = failedAuthLimiter();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Re-read the config because a call named a tool we do not serve.
|
|
87
|
+
*
|
|
88
|
+
* The notify (ADR-029) reaches one instance. A second instance that was warm
|
|
89
|
+
* when the operator connected an account keeps refusing it, which would make
|
|
90
|
+
* "connecting does not need a redeploy" true only sometimes — the worst of
|
|
91
|
+
* the three possible states, because nobody can reproduce it.
|
|
92
|
+
*
|
|
93
|
+
* So the error path closes it. A tool that appeared a moment ago is
|
|
94
|
+
* indistinguishable from a tool that never existed, and both arrive here; one
|
|
95
|
+
* reload tells them apart. Nothing on the success path pays for this.
|
|
96
|
+
*/
|
|
97
|
+
const probeForNewConfig = async (): Promise<boolean> => {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (now - probedAt < RELOAD_PROBE_MS) return false;
|
|
100
|
+
probedAt = now;
|
|
101
|
+
|
|
102
|
+
const result = await options.generations.reload();
|
|
103
|
+
return result.reloaded;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
async fetch(request) {
|
|
108
|
+
// Ahead of everything, including the pre-auth discovery routes: those are
|
|
109
|
+
// exactly what a rebound origin reaches.
|
|
110
|
+
if (options.allowedHostnames) {
|
|
111
|
+
const refusal = rebindingRefusal(request, options.allowedHostnames);
|
|
112
|
+
if (refusal) return refusal;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const url = new URL(request.url);
|
|
116
|
+
|
|
117
|
+
// Before authentication, deliberately: a client's first request is the
|
|
118
|
+
// one that discovers how to authenticate, so requiring a token to read
|
|
119
|
+
// the document that says where tokens come from would close the loop it
|
|
120
|
+
// exists to open.
|
|
121
|
+
if (options.authorization && isAuthorizationPath(url.pathname)) {
|
|
122
|
+
return await handleAuthorization(request, options.authorization);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (url.pathname === '/health') {
|
|
126
|
+
// `status` is unauthenticated because the platform's own probe reads it
|
|
127
|
+
// and a deploy waits on it. The profile *names* are not: on a public URL
|
|
128
|
+
// that is a list of what this endpoint holds, handed to anyone who asks,
|
|
129
|
+
// and `outputs` and `mcp add` — which are the reason it was ever
|
|
130
|
+
// published — both hold the token already.
|
|
131
|
+
const named = await options.authenticator.authenticate(
|
|
132
|
+
request.headers.get('authorization'),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return Response.json({
|
|
136
|
+
status: 'ok',
|
|
137
|
+
...(named.ok
|
|
138
|
+
? { profile: options.primary, profiles: options.generations.current.names() }
|
|
139
|
+
: {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
url.pathname !== MCP_PATH &&
|
|
145
|
+
url.pathname !== ATTACHMENTS_PATH &&
|
|
146
|
+
url.pathname !== RELOAD_PATH
|
|
147
|
+
) {
|
|
148
|
+
return new Response('Not found', { status: 404 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const outcome = await options.authenticator.authenticate(
|
|
152
|
+
request.headers.get('authorization'),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!outcome.ok) {
|
|
156
|
+
options.log.warn('rejected request', { reason: outcome.reason });
|
|
157
|
+
|
|
158
|
+
// After the attempt rather than before it: keyed on the caller alone,
|
|
159
|
+
// anyone able to reach the endpoint could spend the owner's budget and
|
|
160
|
+
// lock them out, which trades a cost problem for a worse availability
|
|
161
|
+
// one. Only a failure consumes a token, so a valid credential is never
|
|
162
|
+
// refused by this.
|
|
163
|
+
const budget = failedAuth.take(callerKey(request), FAILED_AUTH_PER_MINUTE);
|
|
164
|
+
if (!budget.allowed) return tooManyAttempts(budget.retryAfterMs);
|
|
165
|
+
|
|
166
|
+
// The pointer is the whole handshake for a remote client: it reads the
|
|
167
|
+
// named document, finds the authorization server, and starts a flow.
|
|
168
|
+
// Without it the client has to guess the document's location, and a
|
|
169
|
+
// client that guesses wrong reports the endpoint as unreachable.
|
|
170
|
+
const metadata = options.authorization ? resourceMetadataUrl(request) : null;
|
|
171
|
+
|
|
172
|
+
return new Response(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
error: 'unauthorized',
|
|
175
|
+
reason: outcome.reason,
|
|
176
|
+
hint:
|
|
177
|
+
outcome.reason === 'not_configured'
|
|
178
|
+
? 'This profile has no token yet. Run: lanes link token rotate'
|
|
179
|
+
: 'Present the profile token as: Authorization: Bearer <token>',
|
|
180
|
+
}),
|
|
181
|
+
{
|
|
182
|
+
status: 401,
|
|
183
|
+
headers: {
|
|
184
|
+
'content-type': 'application/json',
|
|
185
|
+
'www-authenticate': challenge(metadata),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Behind the same bearer check as everything else, and deliberately not a
|
|
192
|
+
// way to *change* configuration: it re-reads what the CLI already wrote to
|
|
193
|
+
// the store this endpoint boots from, so the one-way flow ADR-004 requires
|
|
194
|
+
// — local CLI to instance — is exactly what it follows. ADR-029.
|
|
195
|
+
if (url.pathname === RELOAD_PATH) {
|
|
196
|
+
const result = await options.generations.reload();
|
|
197
|
+
return Response.json(result);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A request works against one generation for its whole lifetime. Pinning
|
|
201
|
+
// it here rather than reading `current` at each use is what makes a reload
|
|
202
|
+
// landing mid-request invisible to this one: the config it is evaluated
|
|
203
|
+
// against cannot change between two of its own awaits.
|
|
204
|
+
let generation = options.generations.acquire();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Bytes in, handle out — the one thing a tool argument cannot carry,
|
|
208
|
+
// since a file in a tool call is base64 in the model's output. It stages
|
|
209
|
+
// into one named connection rather than a shared area, so a staged file
|
|
210
|
+
// stays as isolated as the account it was staged for.
|
|
211
|
+
if (url.pathname === ATTACHMENTS_PATH) {
|
|
212
|
+
return await stageAttachment({
|
|
213
|
+
profiles: generation.profiles,
|
|
214
|
+
primary: options.primary,
|
|
215
|
+
principal: outcome.principal,
|
|
216
|
+
request,
|
|
217
|
+
clientLabel: request.headers.get('x-mcp-client') ?? undefined,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// After authentication, so an unauthenticated caller cannot make the
|
|
222
|
+
// endpoint poll its store. Before `visible()`, because a skill added
|
|
223
|
+
// since the last poll must not be audited as a refusal on its first call.
|
|
224
|
+
await generation.refreshSkills();
|
|
225
|
+
|
|
226
|
+
const clientLabel = request.headers.get('x-mcp-client') ?? undefined;
|
|
227
|
+
|
|
228
|
+
// Policy-filtered discovery means an unpermitted tool is never
|
|
229
|
+
// advertised, so a call naming one is rejected by the protocol layer
|
|
230
|
+
// before dispatch — and would otherwise leave no trace. The 2026-07-28
|
|
231
|
+
// envelope requires the method and target in headers and rejects any
|
|
232
|
+
// request whose headers and body disagree, so reading them here is exact
|
|
233
|
+
// without parsing (and consuming) the body.
|
|
234
|
+
//
|
|
235
|
+
// **Only for an envelope client.** A 2025-era request carries neither
|
|
236
|
+
// header, and `createMcpHandler` is built without a `legacy` option —
|
|
237
|
+
// whose default is `'stateless'`, so those requests are served rather
|
|
238
|
+
// than refused. This check short-circuits and the refusal goes
|
|
239
|
+
// unrecorded. That is the second documented exception to
|
|
240
|
+
// `audit.every-invocation` in `docs/detailed/security.md`, asserted in
|
|
241
|
+
// `index.test.ts`. Closing it means cloning and parsing the body when
|
|
242
|
+
// the header is absent, which is what `stdio.ts` does for want of
|
|
243
|
+
// headers.
|
|
244
|
+
//
|
|
245
|
+
// `prompts/get` is included because a prompt is named exactly as a tool
|
|
246
|
+
// is — `skills_review-diff` — so the same lookup is exact.
|
|
247
|
+
// `resources/read` is **not**, and cannot be with this shape: 2026-07-28
|
|
248
|
+
// mirrors `params.uri` rather than a name into the header, and a URI does
|
|
249
|
+
// not match a wire name, so including it would record a refusal for every
|
|
250
|
+
// successful read. Recovering a capability id from a concrete URI means
|
|
251
|
+
// matching it against each registered template, which is a real design
|
|
252
|
+
// decision — not least about what to record when it matches nothing —
|
|
253
|
+
// and M4 did not take it. `resources.test.ts` asserts the gap.
|
|
254
|
+
const method = request.headers.get('mcp-method');
|
|
255
|
+
if (method === 'tools/call' || method === 'prompts/get') {
|
|
256
|
+
const toolName = request.headers.get('mcp-name');
|
|
257
|
+
|
|
258
|
+
if (toolName && !generation.visible().has(toolName)) {
|
|
259
|
+
// Before recording it as a refusal: this instance may simply be
|
|
260
|
+
// holding config older than the account the caller is naming.
|
|
261
|
+
if (await probeForNewConfig()) {
|
|
262
|
+
await options.generations.release(generation);
|
|
263
|
+
generation = options.generations.acquire();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (toolName && !generation.visible().has(toolName)) {
|
|
268
|
+
// Recorded against the primary profile: the body has not been read,
|
|
269
|
+
// so which profile the call named is not yet known, and an attempt on
|
|
270
|
+
// a tool no profile advertises belongs to none of them in particular.
|
|
271
|
+
await generation.profiles.get(options.primary)!.dispatcher.recordRefusal({
|
|
272
|
+
principal: outcome.principal,
|
|
273
|
+
capabilityId: capabilityIdForToolName(toolName, generation.allCapabilityIds()),
|
|
274
|
+
clientLabel,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return await generation.handlerFor(outcome.principal, clientLabel).fetch(request);
|
|
280
|
+
} finally {
|
|
281
|
+
await options.generations.release(generation);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async close() {
|
|
286
|
+
await options.generations.close();
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface ServeOptions extends ServerOptions {
|
|
292
|
+
readonly port?: number;
|
|
293
|
+
readonly host?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface RunningServer {
|
|
297
|
+
readonly url: string;
|
|
298
|
+
stop(): Promise<void>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function serve(options: ServeOptions): RunningServer {
|
|
302
|
+
const current: Generation = options.generations.current;
|
|
303
|
+
const primary = current.profiles.get(options.primary);
|
|
304
|
+
if (!primary) throw new Error(`Profile "${options.primary}" is not among those being served.`);
|
|
305
|
+
|
|
306
|
+
// Read once, from the generation that is current at bind time. A reload
|
|
307
|
+
// cannot move a bound socket, so `instance.port` and `instance.host` are
|
|
308
|
+
// deliberately not part of what reloading re-reads (ADR-029).
|
|
309
|
+
const host = options.host ?? primary.config.instance.host;
|
|
310
|
+
const port = options.port ?? primary.config.instance.port;
|
|
311
|
+
|
|
312
|
+
const allowedHostnames = options.allowedHostnames ?? allowedHostnamesFor(host, isLoopback(host));
|
|
313
|
+
const handler = createRequestHandler({
|
|
314
|
+
...options,
|
|
315
|
+
...(allowedHostnames ? { allowedHostnames } : {}),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const server = Bun.serve({
|
|
319
|
+
hostname: host,
|
|
320
|
+
port,
|
|
321
|
+
fetch: (request: Request) => handler.fetch(request),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
url: `http://${host}:${port}${MCP_PATH}`,
|
|
326
|
+
async stop() {
|
|
327
|
+
await handler.close();
|
|
328
|
+
await server.stop(true);
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Logger } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
export type { Logger };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where an endpoint's operational events go.
|
|
7
|
+
*
|
|
8
|
+
* Distinct from the audit log, which records what a caller *did* and is a
|
|
9
|
+
* durable, hash-chained artefact. This is the other half: what happened to
|
|
10
|
+
* requests that never reached dispatch, of which a rejected credential is the
|
|
11
|
+
* one that matters. Audit cannot cover it — a refusal record needs a principal,
|
|
12
|
+
* and failing authentication is precisely not having one.
|
|
13
|
+
*
|
|
14
|
+
* Every caller used to pass an object whose four methods were empty, so the
|
|
15
|
+
* warning at the authentication edge was written and discarded on a public URL.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Discards everything. For tests, whose output should stay readable. */
|
|
19
|
+
export function silentLogger(): Logger {
|
|
20
|
+
return { debug() {}, info() {}, warn() {}, error() {} };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Timestamped lines to whichever stream is handed in.
|
|
25
|
+
*
|
|
26
|
+
* The CLI passes stderr, because stdout is what `--raw` and `--json` callers
|
|
27
|
+
* parse. The container passes stdout, which is where Cloud Run collects logs.
|
|
28
|
+
*/
|
|
29
|
+
export function streamLogger(write: (line: string) => void, now = () => new Date()): Logger {
|
|
30
|
+
const at = (level: string, message: string, detail?: Record<string, unknown>): void => {
|
|
31
|
+
const suffix = detail && Object.keys(detail).length > 0 ? ` ${JSON.stringify(detail)}` : '';
|
|
32
|
+
write(`${now().toISOString()} ${level} ${message}${suffix}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
debug: (message, detail) => at('debug', message, detail),
|
|
37
|
+
info: (message, detail) => at('info', message, detail),
|
|
38
|
+
warn: (message, detail) => at('warn', message, detail),
|
|
39
|
+
error: (message, detail) => at('error', message, detail),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { isPrompt, isResource, isTool } from '#connectivity';
|
|
3
|
+
import { SERVER_ICONS } from './icon.ts';
|
|
4
|
+
import { serverInstructions } from './instructions.ts';
|
|
5
|
+
import { SERVER_NAME } from './naming.ts';
|
|
6
|
+
import { registerPrompt } from './prompts.ts';
|
|
7
|
+
import { registerResource } from './resources.ts';
|
|
8
|
+
import { registerDiscoveredTool, registerLocalTool } from './tools.ts';
|
|
9
|
+
import { mergeCapabilities, type BuildServerOptions } from './visibility.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Building the MCP surface for one principal.
|
|
13
|
+
*
|
|
14
|
+
* Every registration below is downstream of `mergeCapabilities`, which has
|
|
15
|
+
* already applied policy — so this loop never decides what a caller may see, it
|
|
16
|
+
* only decides how each surviving capability is spelled in the protocol.
|
|
17
|
+
*/
|
|
18
|
+
export function buildMcpServer(options: BuildServerOptions): McpServer {
|
|
19
|
+
const names = [...options.profiles.keys()];
|
|
20
|
+
|
|
21
|
+
// One evaluation, two consumers. `mergeCapabilities` runs policy over every
|
|
22
|
+
// capability on every connection, and the instructions describe exactly the
|
|
23
|
+
// set that gets registered below — computing it twice would be the same
|
|
24
|
+
// answer at twice the cost, and two answers the day they drift.
|
|
25
|
+
const merged = mergeCapabilities(options);
|
|
26
|
+
|
|
27
|
+
const server = new McpServer(
|
|
28
|
+
{
|
|
29
|
+
name: SERVER_NAME,
|
|
30
|
+
version: options.version ?? '0.0.0',
|
|
31
|
+
title: `Lanes Link — ${names.join(', ')}`,
|
|
32
|
+
// First argument, and this is the half of the pairing below that is easy
|
|
33
|
+
// to get wrong in the other direction: `icons`, `description` and
|
|
34
|
+
// `websiteUrl` are `Implementation` fields that SEP-973 added, so they
|
|
35
|
+
// belong here and would be dropped from `ServerOptions`.
|
|
36
|
+
//
|
|
37
|
+
// `description` is `package.json`'s, and the test holds the two equal
|
|
38
|
+
// rather than trusting a copy to stay one. It is the whole endpoint being
|
|
39
|
+
// described, not this connection, so it does not name the profiles the
|
|
40
|
+
// way `title` does.
|
|
41
|
+
description: 'A self-hosted MCP gateway for your accounts, memory, skills, and secrets',
|
|
42
|
+
websiteUrl: 'https://github.com/lanes-sh/link',
|
|
43
|
+
icons: SERVER_ICONS,
|
|
44
|
+
},
|
|
45
|
+
// Second argument, not the first: `instructions` is a `ServerOptions` field,
|
|
46
|
+
// and `Implementation` would take it as an unknown extra and drop it from
|
|
47
|
+
// `initialize` without complaining.
|
|
48
|
+
{ instructions: serverInstructions(names, merged) },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const [id, entry] of merged) {
|
|
52
|
+
// Discovered first: an upstream MCP server or an OpenAPI document supplies
|
|
53
|
+
// the schema, and there is no local capability object to inspect.
|
|
54
|
+
if (entry.discovered) {
|
|
55
|
+
registerDiscoveredTool(server, id, entry, options);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const capability = entry.capability;
|
|
60
|
+
if (!capability) continue;
|
|
61
|
+
|
|
62
|
+
if (isTool(capability)) registerLocalTool(server, id, entry, capability, options);
|
|
63
|
+
else if (isResource(capability)) registerResource(server, id, entry, capability, options);
|
|
64
|
+
else if (isPrompt(capability)) registerPrompt(server, id, entry, capability, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return server;
|
|
68
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Icon } from '@modelcontextprotocol/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The mark the endpoint reports to a client that renders one.
|
|
5
|
+
*
|
|
6
|
+
* SEP-973 (protocol revision 2025-11-25) added `icons` to `Implementation`, so
|
|
7
|
+
* `initialize.result.serverInfo` can carry the server's own branding rather
|
|
8
|
+
* than leaving every client to generate a letter avatar from the name.
|
|
9
|
+
*
|
|
10
|
+
* **Nothing renders this in Claude today.** claude.ai shows custom connectors
|
|
11
|
+
* with a generated avatar and ignores `serverInfo.icons` entirely — as does
|
|
12
|
+
* Claude Code. That is a client gap, tracked in anthropics/claude-ai-mcp#152,
|
|
13
|
+
* where the reporter ruled out every server-side route in turn: `icons` with an
|
|
14
|
+
* https src, `icons` with a data URI, `/favicon.ico` and `/favicon.png` at the
|
|
15
|
+
* origin, and an HTML `<link rel="icon">`. None are fetched.
|
|
16
|
+
*
|
|
17
|
+
* It is here anyway because the cost is a few kilobytes on `initialize` and the
|
|
18
|
+
* alternative is a release the day the gap closes. Other clients already
|
|
19
|
+
* honour the field.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The tile: the published mark's own geometry, used verbatim and not inset.
|
|
24
|
+
*
|
|
25
|
+
* These are the numbers from the published file — a 737.28 square at a
|
|
26
|
+
* sub-pixel offset inside a 738 × 739 box, with `rx="150"` corners. The tile is
|
|
27
|
+
* both the ground and the mask, which is what makes the stripes stop exactly
|
|
28
|
+
* where the rounded square does.
|
|
29
|
+
*
|
|
30
|
+
* **The mark is not inset, and that is a deliberate reversal.** An earlier
|
|
31
|
+
* version held it a tenth of the box clear of the edge, reasoning that a
|
|
32
|
+
* circular avatar crop would otherwise take the rounded-square silhouette. It
|
|
33
|
+
* does take it — but what the margin actually produced was a dark ring inside
|
|
34
|
+
* the circle with the stripes shrunk away from it, which reads as a small mark
|
|
35
|
+
* badly placed rather than as a mark whose corners were cropped. Filling the
|
|
36
|
+
* box is the better trade in both shapes: cropped to a circle the stripes run
|
|
37
|
+
* edge to edge, and left square the silhouette is there anyway.
|
|
38
|
+
*/
|
|
39
|
+
const TILE = { x: 0.360107, y: 0.859375, size: 737.28, radius: 150 } as const;
|
|
40
|
+
|
|
41
|
+
/** The box the published file declares, which the tile sits inside. */
|
|
42
|
+
const WIDTH = 738;
|
|
43
|
+
const HEIGHT = 739;
|
|
44
|
+
|
|
45
|
+
const GROUND = '#121214';
|
|
46
|
+
const INK = '#ffffff';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The icon: the published Lanes mark, filling its box, on its own dark ground.
|
|
50
|
+
*
|
|
51
|
+
* This is the mark lanes.sh serves as `/icon-light.svg` and `/icon-dark.svg`.
|
|
52
|
+
* It is *not* the glyph beside "Any MCP client" in the README diagram: that one
|
|
53
|
+
* is the Model Context Protocol's own logo, sitting in the client column next
|
|
54
|
+
* to the Anthropic and OpenAI marks, and shipping it here would have branded
|
|
55
|
+
* this endpoint with someone else's mark.
|
|
56
|
+
*
|
|
57
|
+
* Copied into `src/` rather than read from disk or fetched: the Dockerfile
|
|
58
|
+
* (`src/deployments/gcp/Dockerfile`) copies `package.json`, the lockfile,
|
|
59
|
+
* `bunfig.toml` and `src/` and nothing else, so a runtime read of a checked-in
|
|
60
|
+
* asset outside `src/` works from a checkout and returns nothing from every
|
|
61
|
+
* deployed target — the half of the bug that only shows up in production. A
|
|
62
|
+
* fetch would be worse: an icon that depends on the network resolving is an
|
|
63
|
+
* icon that is sometimes missing.
|
|
64
|
+
*
|
|
65
|
+
* **The ground is painted, not inherited.** The published pair leaves the gaps
|
|
66
|
+
* between the stripes transparent and lets the page supply the ground, which is
|
|
67
|
+
* right for a favicon on lanes.sh and wrong here: a client draws an icon on a
|
|
68
|
+
* surface this file knows nothing about, and transparent gaps mean the mark is
|
|
69
|
+
* a different thing on each one and invisible on some. Painting `#121214`
|
|
70
|
+
* behind the stripes is what makes the result the same icon everywhere, and it
|
|
71
|
+
* is why there is one entry below rather than two.
|
|
72
|
+
*
|
|
73
|
+
* The twenty-four stripes are generated rather than pasted. In the published
|
|
74
|
+
* file they are an exact arithmetic progression — same step on both axes, the
|
|
75
|
+
* export's own rounding never off by more than 0.02 units, which is under a
|
|
76
|
+
* thousandth of a pixel at 32px — so a loop says what twenty-four near-
|
|
77
|
+
* identical lines only imply.
|
|
78
|
+
*/
|
|
79
|
+
function mark(): string {
|
|
80
|
+
const stripes: string[] = [];
|
|
81
|
+
for (let i = 0; i < 24; i += 1) {
|
|
82
|
+
const x = (345.141 + i * 203.2425).toFixed(3);
|
|
83
|
+
const y = (-4382.52 + i * 203.2425).toFixed(3);
|
|
84
|
+
stripes.push(
|
|
85
|
+
`<rect x="${x}" y="${y}" width="143.715" height="6661.17"` +
|
|
86
|
+
` transform="rotate(45 ${x} ${y})" fill="${INK}" />`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// One rect shape, spelled once and used twice: as the ground the stripes are
|
|
91
|
+
// painted over, and as the mask that stops them at its edge. Written apart
|
|
92
|
+
// they are two places to change a corner radius and one place to forget.
|
|
93
|
+
const tile = `x="${TILE.x}" y="${TILE.y}" width="${TILE.size}" height="${TILE.size}" rx="${TILE.radius}"`;
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}" role="img">`,
|
|
97
|
+
'<title>Lanes Link</title>',
|
|
98
|
+
`<rect ${tile} fill="${GROUND}" />`,
|
|
99
|
+
// The rounded square is a mask rather than a drawn shape, which is what
|
|
100
|
+
// makes the stripes stop where they do. Its id has to be stable but not
|
|
101
|
+
// unique — the icon is its own `data:` document, so nothing else is in
|
|
102
|
+
// scope to collide with it.
|
|
103
|
+
`<mask id="tile" maskUnits="userSpaceOnUse" x="0" y="0" width="${WIDTH}" height="${HEIGHT}">`,
|
|
104
|
+
`<rect ${tile} fill="#fff" />`,
|
|
105
|
+
'</mask>',
|
|
106
|
+
`<g mask="url(#tile)">${stripes.join('')}</g>`,
|
|
107
|
+
'</svg>',
|
|
108
|
+
].join('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Base64 rather than percent-encoding, and `Buffer` rather than `btoa`.
|
|
113
|
+
*
|
|
114
|
+
* Both forms are legal in a `data:` URI and the percent-encoded one is smaller
|
|
115
|
+
* and readable in a log, but every example in the spec and every client that
|
|
116
|
+
* has shipped support reads base64 — this is not the field to be clever in.
|
|
117
|
+
*
|
|
118
|
+
* `Buffer.from(…, 'utf8')` for the reason `src/connectivity/auth/basic/index.ts`
|
|
119
|
+
* gives: `btoa` throws above U+00FF and mangles what it does not throw on.
|
|
120
|
+
* Nothing above ASCII is in the mark today, but the failure mode of the day
|
|
121
|
+
* something is would be a corrupt icon rather than an error.
|
|
122
|
+
*/
|
|
123
|
+
function dataUri(svg: string): string {
|
|
124
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One entry, and deliberately no `theme`.
|
|
129
|
+
*
|
|
130
|
+
* `theme` says which UI a client should pick this icon *for*, so it is a claim
|
|
131
|
+
* that there is another one to pick instead. The icon carries its own ground
|
|
132
|
+
* and looks the same on either, and two identical entries labelled `light` and
|
|
133
|
+
* `dark` would be metadata describing a variation that does not exist. If a
|
|
134
|
+
* theme-specific mark is ever wanted, that is the point at which the field
|
|
135
|
+
* earns its place.
|
|
136
|
+
*
|
|
137
|
+
* `sizes: ['any']` is what the spec spells for a scalable icon. SVG is a SHOULD
|
|
138
|
+
* for clients and PNG a MUST, so a client could honour `icons` and still skip
|
|
139
|
+
* this one — a rasterised entry is the fix for that, and it can wait until a
|
|
140
|
+
* client we care about needs it, because a PNG blob checked in here is one
|
|
141
|
+
* nobody can regenerate from a clean checkout.
|
|
142
|
+
*/
|
|
143
|
+
export const SERVER_ICONS: Icon[] = [
|
|
144
|
+
{ src: dataUri(mark()), mimeType: 'image/svg+xml', sizes: ['any'] },
|
|
145
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP surface.
|
|
3
|
+
*
|
|
4
|
+
* Six files, each answering one question:
|
|
5
|
+
*
|
|
6
|
+
* visibility what this principal may see — policy, applied once
|
|
7
|
+
* naming how a capability id is spelled on the wire
|
|
8
|
+
* schema how a vendor's JSON Schema is made safe to publish
|
|
9
|
+
* routing how the profile and connection reach a resource or a prompt
|
|
10
|
+
* instructions what the endpoint says it is for, before any tool is called
|
|
11
|
+
* tools / resources / prompts how each capability kind is registered
|
|
12
|
+
* build the loop that puts them together
|
|
13
|
+
*
|
|
14
|
+
* It was one 685-line file, and the seam that mattered was invisible in it:
|
|
15
|
+
* everything in `visibility` decides *what* is exposed and everything else
|
|
16
|
+
* decides *how*. A discovery leak can only come from the first, which is now
|
|
17
|
+
* one file with one entry point.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { buildMcpServer } from './build.ts';
|
|
21
|
+
export { serverInstructions } from './instructions.ts';
|
|
22
|
+
export { SERVER_NAME, capabilityIdForToolName, toolNameFor } from './naming.ts';
|
|
23
|
+
export { scopeResourceUri } from './routing.ts';
|
|
24
|
+
export { sanitizeSchema } from './schema.ts';
|
|
25
|
+
export {
|
|
26
|
+
mergeCapabilities,
|
|
27
|
+
oneProfile,
|
|
28
|
+
visibleCapabilities,
|
|
29
|
+
type BuildServerOptions,
|
|
30
|
+
type MergedCapability,
|
|
31
|
+
type ProfileRuntime,
|
|
32
|
+
} from './visibility.ts';
|