@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,400 @@
|
|
|
1
|
+
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
|
|
2
|
+
import {
|
|
3
|
+
AuthenticatorChain,
|
|
4
|
+
BearerAuthenticator,
|
|
5
|
+
IssuedTokenAuthenticator,
|
|
6
|
+
OAuthServer,
|
|
7
|
+
OAuthStore,
|
|
8
|
+
tokensMatch,
|
|
9
|
+
} from '#auth';
|
|
10
|
+
import { oneProfile, type ProfileRuntime } from './mcp/index.ts';
|
|
11
|
+
import { parseConfig, type Config } from '#profile';
|
|
12
|
+
import { ProviderRegistry, toPolicyDocument } from '#registry';
|
|
13
|
+
import { Dispatcher } from '#dispatch';
|
|
14
|
+
import { createMemoryCredentials, createMemoryState } from '#stores/state/testing.ts';
|
|
15
|
+
import { RateLimiter } from '#policy';
|
|
16
|
+
import { silentLogger, type Logger } from './logging.ts';
|
|
17
|
+
import { createMemoryBlobStore } from '#stores/blobs/testing.ts';
|
|
18
|
+
import { exampleProvider } from '#providers/example/provider.ts';
|
|
19
|
+
import { createLocalConnector } from '#connectivity/transports';
|
|
20
|
+
import type { RuntimeState } from '#stores/state';
|
|
21
|
+
import type { AuditStore } from '#audit';
|
|
22
|
+
import { createBlobAuditStore } from '#deployments/adapters/audit-blob.ts';
|
|
23
|
+
import type { AnyConnector, ProviderDefinition } from '#connectivity';
|
|
24
|
+
import { serve, type RunningServer } from './index.ts';
|
|
25
|
+
import { Generations } from './generations.ts';
|
|
26
|
+
import { serveOverStdio } from './stdio.ts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Test harness: a fully wired profile served over real HTTP on a real port.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately not a mock of the transport. The point of these tests is that
|
|
32
|
+
* an agent talking to the actual endpoint sees the right tools and is refused
|
|
33
|
+
* the right calls, and a mocked transport cannot demonstrate that.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const TEST_TOKEN = 'llk_test_token_value';
|
|
37
|
+
|
|
38
|
+
export function configFor(profile: string, port: number, policy: string): Config {
|
|
39
|
+
return parseConfig(`
|
|
40
|
+
contract: 1
|
|
41
|
+
instance:
|
|
42
|
+
profile: ${profile}
|
|
43
|
+
default_target: local
|
|
44
|
+
port: ${port}
|
|
45
|
+
targets:
|
|
46
|
+
local:
|
|
47
|
+
credentials: { adapter: file, path: ./data/${profile}/credentials.enc }
|
|
48
|
+
storage: { adapter: filesystem, path: ./data/${profile} }
|
|
49
|
+
limits:
|
|
50
|
+
requests_per_minute: 1000
|
|
51
|
+
upstream_calls_per_minute: 1000
|
|
52
|
+
connections:
|
|
53
|
+
- id: a
|
|
54
|
+
provider: example
|
|
55
|
+
account: Scratch A
|
|
56
|
+
- id: b
|
|
57
|
+
provider: example
|
|
58
|
+
account: Scratch B
|
|
59
|
+
policy:
|
|
60
|
+
${policy}
|
|
61
|
+
`).config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Harness {
|
|
65
|
+
readonly server: RunningServer;
|
|
66
|
+
readonly state: RuntimeState;
|
|
67
|
+
readonly audit: AuditStore;
|
|
68
|
+
readonly token: string;
|
|
69
|
+
/**
|
|
70
|
+
* The primary profile's dispatcher.
|
|
71
|
+
*
|
|
72
|
+
* Exposed because policy denial and MCP discovery filtering are two different
|
|
73
|
+
* observations of one decision, and only one of them is visible over the
|
|
74
|
+
* wire: a capability a profile cannot reach is never registered, so calling
|
|
75
|
+
* it is answered by the SDK before any of our code runs. Asserting that the
|
|
76
|
+
* *denial* is recorded means asking the layer that records it.
|
|
77
|
+
*/
|
|
78
|
+
readonly dispatcher: Dispatcher;
|
|
79
|
+
/** The generation holder, so a test can drive a reload without an HTTP call. */
|
|
80
|
+
readonly generations: Generations;
|
|
81
|
+
stop(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { parseConfig } from '#profile';
|
|
85
|
+
|
|
86
|
+
export interface HarnessOptions {
|
|
87
|
+
profile: string;
|
|
88
|
+
log?: Logger;
|
|
89
|
+
port: number;
|
|
90
|
+
policy: string;
|
|
91
|
+
token?: string;
|
|
92
|
+
/** Extra providers to register beyond `example`. */
|
|
93
|
+
providers?: readonly ProviderDefinition[];
|
|
94
|
+
/** Credentials to seed, beyond the profile token. */
|
|
95
|
+
credentials?: Record<string, string>;
|
|
96
|
+
/** Overrides the generated config wholesale, for providers needing more than the default. */
|
|
97
|
+
config?: Config;
|
|
98
|
+
/** Extra profiles this one endpoint also serves, each with its own policy. */
|
|
99
|
+
alsoServe?: ReadonlyArray<{ profile: string; policy: string }>;
|
|
100
|
+
/**
|
|
101
|
+
* What the endpoint calls to re-read skills before serving a request.
|
|
102
|
+
*
|
|
103
|
+
* Handed the registry so a test can swap the skills provider in it, which is
|
|
104
|
+
* what `openRuntime` does for real. Passed in rather than built here because
|
|
105
|
+
* where skills are stored is the runtime's business, not the transport's.
|
|
106
|
+
*/
|
|
107
|
+
refreshSkills?: (registry: ProviderRegistry) => Promise<void>;
|
|
108
|
+
/** Serve the `self` authorization flow alongside the bearer token. */
|
|
109
|
+
authorization?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* What a reload re-reads, standing in for `openReconciled` over a workspace
|
|
112
|
+
* this harness does not have. Throwing is how the "a failed reload keeps
|
|
113
|
+
* serving the old generation" case is reached; absent means nothing new.
|
|
114
|
+
*/
|
|
115
|
+
reopen?: () => Promise<ReadonlyMap<string, ProfileRuntime>>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Everything a transport needs, wired from one set of options.
|
|
120
|
+
*
|
|
121
|
+
* Extracted when stdio arrived: the two transports differ in how a caller
|
|
122
|
+
* reaches the profiles, and in nothing else. A second copy of this wiring would
|
|
123
|
+
* have let them drift, which is exactly what the stdio tests are for.
|
|
124
|
+
*/
|
|
125
|
+
interface WiredProfiles {
|
|
126
|
+
readonly profiles: Map<string, ProfileRuntime>;
|
|
127
|
+
readonly state: RuntimeState;
|
|
128
|
+
/** The real blob-backed log over an in-memory store, not a second fake. */
|
|
129
|
+
readonly audit: AuditStore;
|
|
130
|
+
readonly dispatcher: Dispatcher;
|
|
131
|
+
readonly credentials: ReturnType<typeof createMemoryCredentials>;
|
|
132
|
+
readonly token: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function wireProfiles(options: HarnessOptions): WiredProfiles {
|
|
136
|
+
const config = options.config ?? configFor(options.profile, options.port, options.policy);
|
|
137
|
+
const token = options.token ?? TEST_TOKEN;
|
|
138
|
+
|
|
139
|
+
const state = createMemoryState();
|
|
140
|
+
const audit = createBlobAuditStore({ storage: createMemoryBlobStore() });
|
|
141
|
+
const credentials = createMemoryCredentials({ 'profile/token': token, ...options.credentials });
|
|
142
|
+
// `allowReserved` for the same reason `buildRegistry` passes it: the owner
|
|
143
|
+
// layer claims `memory`, `skills`, and `vault`, and the guard still refuses
|
|
144
|
+
// them everywhere else.
|
|
145
|
+
const registry = new ProviderRegistry({ allowReserved: true });
|
|
146
|
+
registry.register(exampleProvider);
|
|
147
|
+
for (const provider of options.providers ?? []) registry.register(provider);
|
|
148
|
+
|
|
149
|
+
const policy = toPolicyDocument(config);
|
|
150
|
+
|
|
151
|
+
const dispatcher = new Dispatcher({
|
|
152
|
+
config,
|
|
153
|
+
registry,
|
|
154
|
+
connectorFor: (providerId): AnyConnector | undefined => {
|
|
155
|
+
const entry = registry.get(providerId);
|
|
156
|
+
return entry?.definition ? createLocalConnector(entry.definition) : undefined;
|
|
157
|
+
},
|
|
158
|
+
policy,
|
|
159
|
+
state,
|
|
160
|
+
audit,
|
|
161
|
+
credentials,
|
|
162
|
+
storage: createMemoryBlobStore(),
|
|
163
|
+
limiter: new RateLimiter(),
|
|
164
|
+
log: silentLogger(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Each extra profile gets its own state and its own log, so the isolation
|
|
168
|
+
// the tests assert is real rather than a naming convention.
|
|
169
|
+
const profiles = new Map(
|
|
170
|
+
oneProfile(options.profile, {
|
|
171
|
+
config,
|
|
172
|
+
registry,
|
|
173
|
+
dispatcher,
|
|
174
|
+
policy,
|
|
175
|
+
...(options.refreshSkills ? { refreshSkills: () => options.refreshSkills!(registry) } : {}),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (const extra of options.alsoServe ?? []) {
|
|
180
|
+
const extraConfig = configFor(extra.profile, options.port, extra.policy);
|
|
181
|
+
const extraPolicy = toPolicyDocument(extraConfig);
|
|
182
|
+
const extraDatabase = createMemoryState();
|
|
183
|
+
|
|
184
|
+
profiles.set(extra.profile, {
|
|
185
|
+
config: extraConfig,
|
|
186
|
+
registry,
|
|
187
|
+
policy: extraPolicy,
|
|
188
|
+
dispatcher: new Dispatcher({
|
|
189
|
+
config: extraConfig,
|
|
190
|
+
registry,
|
|
191
|
+
connectorFor: (providerId): AnyConnector | undefined => {
|
|
192
|
+
const entry = registry.get(providerId);
|
|
193
|
+
return entry?.definition ? createLocalConnector(entry.definition) : undefined;
|
|
194
|
+
},
|
|
195
|
+
policy: extraPolicy,
|
|
196
|
+
state: extraDatabase,
|
|
197
|
+
audit: createBlobAuditStore({ storage: createMemoryBlobStore() }),
|
|
198
|
+
credentials,
|
|
199
|
+
storage: createMemoryBlobStore(),
|
|
200
|
+
limiter: new RateLimiter(),
|
|
201
|
+
log: silentLogger(),
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { profiles, state, audit, dispatcher, credentials, token };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function startHarness(options: HarnessOptions): Harness {
|
|
210
|
+
const { profiles, state, audit, dispatcher, credentials, token } = wireProfiles(options);
|
|
211
|
+
|
|
212
|
+
const bearer = new BearerAuthenticator({
|
|
213
|
+
profile: options.profile,
|
|
214
|
+
tokenRef: 'profile/token',
|
|
215
|
+
credentials,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// The real wiring from `endpoint.ts`, not a stand-in: the flow under test is
|
|
219
|
+
// the one a connector drives over HTTP, and a fake authorization server would
|
|
220
|
+
// demonstrate that the fake works.
|
|
221
|
+
const store = options.authorization ? new OAuthStore(state.kv) : null;
|
|
222
|
+
const gate = store
|
|
223
|
+
? {
|
|
224
|
+
surface: {
|
|
225
|
+
server: new OAuthServer({
|
|
226
|
+
store,
|
|
227
|
+
accessTokenTtlMs: 3_600_000,
|
|
228
|
+
verifyOwner: (presented) => Promise.resolve(tokensMatch(presented, token)),
|
|
229
|
+
}),
|
|
230
|
+
issuer: (origin: string) => origin,
|
|
231
|
+
mcpPath: '/mcp',
|
|
232
|
+
target: 'local',
|
|
233
|
+
},
|
|
234
|
+
authenticator: new IssuedTokenAuthenticator(store, options.profile),
|
|
235
|
+
}
|
|
236
|
+
: null;
|
|
237
|
+
|
|
238
|
+
const log = options.log ?? silentLogger();
|
|
239
|
+
|
|
240
|
+
const nothing = () => Promise.resolve();
|
|
241
|
+
const generations = new Generations(
|
|
242
|
+
{ profiles, close: nothing },
|
|
243
|
+
async () => ({ profiles: (await options.reopen?.()) ?? profiles, close: nothing }),
|
|
244
|
+
{ primary: options.profile, log },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const server = serve({
|
|
248
|
+
generations,
|
|
249
|
+
primary: options.profile,
|
|
250
|
+
authenticator: gate ? new AuthenticatorChain([bearer, gate.authenticator]) : bearer,
|
|
251
|
+
...(gate ? { authorization: gate.surface } : {}),
|
|
252
|
+
log,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
server,
|
|
257
|
+
state,
|
|
258
|
+
audit,
|
|
259
|
+
token,
|
|
260
|
+
dispatcher,
|
|
261
|
+
generations,
|
|
262
|
+
stop: () => server.stop(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface StdioHarness {
|
|
267
|
+
/** A real MCP client, connected to the real surface over a linked transport pair. */
|
|
268
|
+
readonly client: Client;
|
|
269
|
+
readonly state: RuntimeState;
|
|
270
|
+
readonly audit: AuditStore;
|
|
271
|
+
readonly dispatcher: Dispatcher;
|
|
272
|
+
stop(): Promise<void>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The same profiles, served over stdio instead of a port.
|
|
277
|
+
*
|
|
278
|
+
* `InMemoryTransport.createLinkedPair()` stands in for the pipe: the surface
|
|
279
|
+
* gets one end, a real `Client` gets the other, and nothing about the surface
|
|
280
|
+
* knows the difference — `serveOverStdio` takes the transport for exactly this
|
|
281
|
+
* reason. What is not exercised is the process boundary itself, which is the
|
|
282
|
+
* client's business rather than ours.
|
|
283
|
+
*/
|
|
284
|
+
export async function startStdioHarness(
|
|
285
|
+
options: HarnessOptions & { clientLabel?: string },
|
|
286
|
+
): Promise<StdioHarness> {
|
|
287
|
+
const { profiles, state, audit, dispatcher } = wireProfiles(options);
|
|
288
|
+
const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
|
|
289
|
+
|
|
290
|
+
const surface = serveOverStdio({
|
|
291
|
+
profiles,
|
|
292
|
+
primary: options.profile,
|
|
293
|
+
log: silentLogger(),
|
|
294
|
+
transport: serverSide,
|
|
295
|
+
...(options.clientLabel ? { clientLabel: options.clientLabel } : {}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const client = new Client({ name: options.clientLabel ?? 'lanes-link-stdio-test', version: '0.0.0' });
|
|
299
|
+
await client.connect(clientSide);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
client,
|
|
303
|
+
state,
|
|
304
|
+
audit,
|
|
305
|
+
dispatcher,
|
|
306
|
+
async stop() {
|
|
307
|
+
await client.close();
|
|
308
|
+
await surface.close();
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface RpcResult {
|
|
314
|
+
readonly status: number;
|
|
315
|
+
readonly body: Record<string, unknown>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* One JSON-RPC call over the wire.
|
|
320
|
+
*
|
|
321
|
+
* Handles both plain JSON and SSE-framed responses, because a streamable HTTP
|
|
322
|
+
* server may answer either and a test that only understands one would fail for
|
|
323
|
+
* the wrong reason.
|
|
324
|
+
*/
|
|
325
|
+
export async function rpc(
|
|
326
|
+
url: string,
|
|
327
|
+
method: string,
|
|
328
|
+
params: Record<string, unknown>,
|
|
329
|
+
options: { token?: string | null; clientLabel?: string; id?: number } = {},
|
|
330
|
+
): Promise<RpcResult> {
|
|
331
|
+
const headers: Record<string, string> = {
|
|
332
|
+
'content-type': 'application/json',
|
|
333
|
+
accept: 'application/json, text/event-stream',
|
|
334
|
+
'mcp-protocol-version': '2026-07-28',
|
|
335
|
+
// 2026-07-28 requires the method — and, where the payload names one, the
|
|
336
|
+
// target — in headers as well as in the body, so an intermediary can route
|
|
337
|
+
// without parsing the payload. The server rejects the request if the two
|
|
338
|
+
// disagree, which is what makes the header trustworthy.
|
|
339
|
+
'mcp-method': method,
|
|
340
|
+
};
|
|
341
|
+
// 2026-07-28 mirrors `params.name` for `tools/call` and `prompts/get`, and
|
|
342
|
+
// `params.uri` for `resources/read`, into `Mcp-Name`. The server rejects the
|
|
343
|
+
// request when the two disagree — including when the header is simply absent
|
|
344
|
+
// — so a resource test that only mirrored `name` would fail for the wrong
|
|
345
|
+
// reason.
|
|
346
|
+
const named = params['name'] ?? params['uri'];
|
|
347
|
+
if (typeof named === 'string') headers['mcp-name'] = named;
|
|
348
|
+
if (options.token !== null) headers['authorization'] = `Bearer ${options.token ?? TEST_TOKEN}`;
|
|
349
|
+
if (options.clientLabel) headers['x-mcp-client'] = options.clientLabel;
|
|
350
|
+
|
|
351
|
+
const response = await fetch(url, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers,
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
jsonrpc: '2.0',
|
|
356
|
+
id: options.id ?? 1,
|
|
357
|
+
method,
|
|
358
|
+
params: {
|
|
359
|
+
...params,
|
|
360
|
+
// The 2026-07-28 envelope: every request declares its protocol
|
|
361
|
+
// version and the caller's capabilities, since there is no
|
|
362
|
+
// handshake left to establish them once.
|
|
363
|
+
_meta: {
|
|
364
|
+
'io.modelcontextprotocol/protocolVersion': '2026-07-28',
|
|
365
|
+
'io.modelcontextprotocol/clientCapabilities': {},
|
|
366
|
+
'io.modelcontextprotocol/clientInfo': {
|
|
367
|
+
name: options.clientLabel ?? 'lanes-link-test',
|
|
368
|
+
version: '0.0.0',
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const text = await response.text();
|
|
376
|
+
return { status: response.status, body: parseBody(text) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseBody(text: string): Record<string, unknown> {
|
|
380
|
+
if (text.trim().length === 0) return {};
|
|
381
|
+
|
|
382
|
+
if (text.startsWith('event:') || text.includes('\ndata:') || text.startsWith('data:')) {
|
|
383
|
+
const line = text
|
|
384
|
+
.split('\n')
|
|
385
|
+
.find((candidate) => candidate.startsWith('data:'));
|
|
386
|
+
return line ? (JSON.parse(line.slice('data:'.length).trim()) as Record<string, unknown>) : {};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
391
|
+
} catch {
|
|
392
|
+
return { raw: text };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Port allocator, so parallel test files never collide. */
|
|
397
|
+
let nextPort = 7900;
|
|
398
|
+
export function allocatePort(): number {
|
|
399
|
+
return nextPort++;
|
|
400
|
+
}
|