@lenso/mcp 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -10
- package/dist/adapter.d.ts +29 -0
- package/dist/borrowed-stdio.d.ts +13 -0
- package/dist/http.d.ts +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +908 -43
- package/dist/protocol.d.ts +11 -0
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -1,11 +1,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MCP adapters
|
|
2
2
|
|
|
3
|
-
`@lenso/mcp`
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`@lenso/mcp` projects **explicitly selected existing Operations**, never inferred
|
|
4
|
+
service methods. It offers a borrowed runtime adapter, dedicated local stdio,
|
|
5
|
+
and an opt-in authenticated Fetch Streamable HTTP entry. HTTP starts no listener
|
|
6
|
+
and does not change host logging. No business handlers, OAuth authorization
|
|
7
|
+
server, shell, Agent loop, tasks, or durable cancellation are supplied.
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Borrow one running app
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createMcpAdapter, serveBorrowedStdio } from "@lenso/mcp";
|
|
13
|
+
import { selectManageOperations } from "@lenso/manage";
|
|
14
|
+
|
|
15
|
+
const shared = {
|
|
16
|
+
running: app, // already started by its owner
|
|
17
|
+
plugins: [notes], // exact installed instances
|
|
18
|
+
operations: selectManageOperations(notesManage, ["read", "write"]),
|
|
19
|
+
canList: (operation, request) => policy.canSee(request.identity, operation),
|
|
20
|
+
authorize: (operation, request) => policy.canInvoke(request.identity, operation),
|
|
21
|
+
binding: (_operation, _input, request) => ({
|
|
22
|
+
context: { identity: request.identity, requestId: request.requestId, signal: request.signal },
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
const adapter = await createMcpAdapter(shared);
|
|
26
|
+
// adapter.listTools({identity, requestId, signal})
|
|
27
|
+
// adapter.callTool(discoveredName, input, {identity, requestId, signal})
|
|
28
|
+
await adapter.close(); // app remains usable; its owner alone stops it
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`running` is the existing Engine `OperationRuntime`; `binding` returns existing
|
|
32
|
+
`OperationBoundOptions`, including confirmation/approval callbacks where required.
|
|
33
|
+
It must match the real service's second parameter (`context: true`). The request
|
|
34
|
+
context is trusted entry data, not a new registration or actor-in-JSON protocol.
|
|
35
|
+
Every call rechecks both visibility and invocation permission. The ordinary
|
|
36
|
+
service still enforces its tenant/object/owner rules. Manage/`createAgentTools`
|
|
37
|
+
are the canonical schema/metadata source; all selected schemas must convert to
|
|
38
|
+
SDK-compatible object schemas before readiness, including currently hidden tools.
|
|
39
|
+
|
|
40
|
+
The borrowed adapter prepares one immutable Manage selection for its lifetime.
|
|
41
|
+
Discovery, invocation policies and bindings receive the same frozen declaration
|
|
42
|
+
snapshots, retaining exact plugin/schema/function identities. Do not key policies
|
|
43
|
+
by the original declaration object's identity; use the exact plugin and declared
|
|
44
|
+
method. Original declaration edits do not change admission or dispatch. Replace
|
|
45
|
+
the adapter when changing its selection. Existing `operation_N` tool names remain
|
|
46
|
+
entry-local, translated internally to Manage's selection-scoped opaque keys.
|
|
47
|
+
Close revokes admission, waits for actual work to drain, then releases the
|
|
48
|
+
selection's runtime references; policy closures retain any resources they capture.
|
|
49
|
+
|
|
50
|
+
For a **dedicated local process**, replace the adapter creation above with:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const stdio = await serveBorrowedStdio({ ...shared, identity: trustedLaunchIdentity });
|
|
54
|
+
// On shutdown await stdio.close(), then let the entry owner stop app.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It reuses that app for all calls, reserves stdio process ownership, routes console
|
|
58
|
+
logs to redacted stderr, and drains on EOF/SIGINT/SIGTERM. Start the app with a
|
|
59
|
+
stderr logger: the adapter cannot redirect logs emitted before it starts.
|
|
60
|
+
Never embed this stdio owner in a general-purpose HTTP host.
|
|
61
|
+
|
|
62
|
+
## Explicit HTTP mount and identity
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createHttpMcp } from "@lenso/mcp";
|
|
66
|
+
const mcp = await createHttpMcp({
|
|
67
|
+
...shared,
|
|
68
|
+
resource: "https://api.example.com/mcp",
|
|
69
|
+
authorizationServers: ["https://idp.example.com"],
|
|
70
|
+
requiredScopes: ["mcp:use"],
|
|
71
|
+
allowedOrigins: ["https://trusted-client.example.com"], // [] denies present Origins
|
|
72
|
+
verifyToken: hostIdentityProvider.verifyMcpAccessToken,
|
|
73
|
+
});
|
|
74
|
+
// Mount both /mcp and /.well-known/oauth-protected-resource/mcp:
|
|
75
|
+
// host Fetch router delegates these paths to mcp.fetch(request).
|
|
76
|
+
// Host owns its existing listener, TLS, CORS and reverse-proxy configuration.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`verifyToken(token, signal)` is mandatory. Inject the host's real OAuth/OIDC
|
|
80
|
+
signature verifier or token introspection client; decoding a JWT is not
|
|
81
|
+
verification. For JWT providers, a host-installed `jose` verifier can use
|
|
82
|
+
`createRemoteJWKSet` and `jwtVerify` with a fixed issuer, this resource audience,
|
|
83
|
+
and an explicit algorithm allowlist. Require `exp`; honor `nbf`, provider
|
|
84
|
+
revocation/current grants, and abortable verification. Map the **verified**
|
|
85
|
+
subject to the host's tenant and Lenso business identity. Do not trust a
|
|
86
|
+
client-selected tenant claim without the provider/host membership policy.
|
|
87
|
+
When services require an `@lenso/auth` Actor, retain the original verified Actor
|
|
88
|
+
reference in an identity extension such as `identity.actor` and bind that exact
|
|
89
|
+
reference. A copied MCP identity is not an Auth Actor or permission grant.
|
|
90
|
+
|
|
91
|
+
Return `McpIdentity`: `{subject, tenant, issuer, audience: string[], scopes:
|
|
92
|
+
string[], expiresAt}` (Unix seconds), optionally extending it with verified
|
|
93
|
+
business evidence. The adapter rechecks issuer, resource audience, expiry and
|
|
94
|
+
required scopes on **every** protected request, including notifications and
|
|
95
|
+
DELETE. `canList`, `authorize` and service policy handle current business grants;
|
|
96
|
+
discovery is not call permission. Configure the issuer string exactly, including
|
|
97
|
+
any provider-required trailing slash. Production verification/revocation remains
|
|
98
|
+
the host's responsibility, not the example's deterministic fixture verifier.
|
|
99
|
+
|
|
100
|
+
Missing/invalid credentials return 401 and a Bearer `resource_metadata` challenge;
|
|
101
|
+
missing required scopes return 403 with `insufficient_scope`. Public RFC 9728
|
|
102
|
+
metadata advertises the configured resource and authorization servers. Tokens
|
|
103
|
+
are neither forwarded to services nor to a different resource. Session IDs never
|
|
104
|
+
authenticate. Sessions and rate buckets bind issuer/subject/tenant/resource;
|
|
105
|
+
foreign owners receive 404 and grants are not cached in sessions.
|
|
106
|
+
|
|
107
|
+
Only canonical configured Host/Origin values are accepted. HTTPS is required
|
|
108
|
+
except on loopback; forwarded headers are not trusted. A proxy host must present
|
|
109
|
+
the canonical public Request URL through its own trusted routing. The adapter
|
|
110
|
+
starts no second listener; an independent listener must explicitly bind loopback
|
|
111
|
+
unless the host deliberately configures remote exposure.
|
|
112
|
+
|
|
113
|
+
## Borrowed-entry bounds and protocol support
|
|
114
|
+
|
|
115
|
+
Defaults: four admitted calls/discoveries (no queue), 30s request timeout,
|
|
116
|
+
256 KiB business input/catalog, 1 MiB business result, 1 MiB HTTP frame, and
|
|
117
|
+
2 MiB + 4 KiB complete HTTP response (including cumulative internal stream data).
|
|
118
|
+
HTTP additionally admits 32 requests, 128 sessions, 5-minute idle expiry, and
|
|
119
|
+
120 requests/minute per identity/tenant with at most 1024 rate buckets.
|
|
120
|
+
Trusted options can change these budgets; output/HTTP error budgets must fit a
|
|
121
|
+
256-byte fixed diagnostic. HTTP's outer verification/body/transport deadline is
|
|
122
|
+
the request timeout plus 1s. Expired sessions/buckets are swept on ingress.
|
|
123
|
+
|
|
124
|
+
Cancellation and timeout reach binding signals; only a host-bound cooperative
|
|
125
|
+
service can interrupt actual work. Noncooperative work retains its concurrency
|
|
126
|
+
slot until settlement. `close()` stops admission, aborts owned request signals,
|
|
127
|
+
drains actual work and releases protocol resources; it is idempotent and never
|
|
128
|
+
calls `app.stop()`. A hanging noncooperative service/verifier can prolong drain.
|
|
129
|
+
Neither cancellation nor a lost response promises rollback or safe retry.
|
|
130
|
+
HTTP disconnect alone is deliberately not an MCP cancellation notification.
|
|
131
|
+
|
|
132
|
+
Protocol failures use fixed SDK MCP errors. Validation, authorization, business
|
|
133
|
+
failure, cancellation, timeout, busy/closed and size errors have fixed bounded
|
|
134
|
+
tool diagnostics; arbitrary thrown text, stacks, input and credentials are omitted.
|
|
135
|
+
Engine's finite JSON/redaction policy remains in force, not a sandbox.
|
|
136
|
+
|
|
137
|
+
Official SDK **1.32.1** remains exactly pinned. Official-client tests verify
|
|
138
|
+
`2025-11-25` and `2025-03-26` Streamable HTTP, plus existing stdio. SDK owns
|
|
139
|
+
negotiation, messages, session IDs and notification validation. Public HTTP
|
|
140
|
+
responses are finite JSON; GET returns 405 (no standalone SSE), DELETE terminates
|
|
141
|
+
sessions. No HTTP+SSE legacy endpoint, replay/event store, progress, resources,
|
|
142
|
+
prompts, elicitation or task capability is claimed.
|
|
143
|
+
|
|
144
|
+
Two version-specific mitigations use SDK public APIs: its finite POST SSE
|
|
145
|
+
transport is consumed within a budget and normalized to JSON because 1.32.1
|
|
146
|
+
JSON mode retains completed stream resolvers; entry-scoped cancellation handlers
|
|
147
|
+
handle valid IDs `0`/`""` and emit finite replies so cancellation does not retain
|
|
148
|
+
HTTP waiters. Regression tests inspect retained SDK state, cancellation and
|
|
149
|
+
escape-heavy JSON. No SDK private state is patched.
|
|
150
|
+
|
|
151
|
+
See the [offline host example](../../examples/mcp-host/README.md) for copyable
|
|
152
|
+
workspace commands and discovery/multiple read/write calls with one app.
|
|
153
|
+
These new exports are source-workspace changes, not proof of registry publication.
|
|
154
|
+
|
|
155
|
+
## Existing trusted local launch (unchanged)
|
|
9
156
|
|
|
10
157
|
Install the optional package in the application and create a dedicated entry:
|
|
11
158
|
|
|
@@ -56,7 +203,7 @@ processes with separately authorized credentials when local callers require
|
|
|
56
203
|
identity isolation. A developer who can change application code/config or use
|
|
57
204
|
the raw DB has administrative authority outside the tool surface; this adapter
|
|
58
205
|
does not grant that authority to its client. Remote credentials, audiences and
|
|
59
|
-
scopes
|
|
206
|
+
scopes belong to the explicitly enabled HTTP entry, never this stdio entry.
|
|
60
207
|
|
|
61
208
|
Successful tool content contains one text block with Engine's deterministic,
|
|
62
209
|
redacted JSON result. No output schema is fabricated. Undefined, cyclic,
|
|
@@ -129,13 +276,15 @@ Primary sources checked for this implementation:
|
|
|
129
276
|
|
|
130
277
|
This package deliberately uses the mature official monolithic v1 SDK. It does
|
|
131
278
|
**not** claim implementation of the newer 2026-07-28 stateless protocol or
|
|
132
|
-
v2-only features. No resources, prompts, tasks
|
|
133
|
-
|
|
279
|
+
v2-only features. No resources, prompts, tasks or elicitation are advertised.
|
|
280
|
+
HTTP uses the [2025-11-25 authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
|
|
281
|
+
and RFC 9728 protected-resource metadata, not an application login endpoint.
|
|
134
282
|
|
|
135
283
|
## Development
|
|
136
284
|
|
|
137
285
|
The integration owner installs dependencies and owns the workspace lockfile.
|
|
138
|
-
Rebuild `@lenso/core`, `@lenso/engine`,
|
|
286
|
+
Rebuild `@lenso/core`, `@lenso/engine`, `@lenso/web`, `@lenso/auth`,
|
|
287
|
+
`@lenso/manage`, and `@lenso/cli` before consuming their exports.
|
|
139
288
|
Then run from this package:
|
|
140
289
|
|
|
141
290
|
```sh
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Plugin } from "@lenso/core";
|
|
2
|
+
import { type Operation, type OperationBoundOptions, type OperationRuntime } from "@lenso/engine/operations";
|
|
3
|
+
import { type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
export interface McpRequestContext<I> {
|
|
5
|
+
readonly identity: I;
|
|
6
|
+
readonly requestId: string;
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export interface McpAdapterOptions<I, O extends Operation = Operation> {
|
|
10
|
+
readonly running: OperationRuntime;
|
|
11
|
+
readonly plugins: readonly Plugin<unknown>[];
|
|
12
|
+
readonly operations: readonly O[];
|
|
13
|
+
readonly canList: (operation: O, request: McpRequestContext<I>) => boolean | Promise<boolean>;
|
|
14
|
+
readonly authorize: (operation: O, request: McpRequestContext<I>) => boolean | Promise<boolean>;
|
|
15
|
+
readonly binding: (operation: O, input: unknown, request: McpRequestContext<I>) => OperationBoundOptions<NoInfer<O>> | Promise<OperationBoundOptions<NoInfer<O>>>;
|
|
16
|
+
readonly maxInputBytes?: number;
|
|
17
|
+
readonly maxOutputBytes?: number;
|
|
18
|
+
readonly maxCatalogBytes?: number;
|
|
19
|
+
readonly maxConcurrentCalls?: number;
|
|
20
|
+
readonly requestTimeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Borrows only runtime capabilities, never app start/stop ownership. */
|
|
23
|
+
export declare function createMcpAdapter<I, O extends Operation = Operation>(options: McpAdapterOptions<I, O>): Promise<Readonly<{
|
|
24
|
+
listTools(request: McpRequestContext<I>): Promise<{
|
|
25
|
+
tools: Tool[];
|
|
26
|
+
}>;
|
|
27
|
+
callTool(name: string, input: unknown, request: McpRequestContext<I>): Promise<CallToolResult>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Operation } from "@lenso/engine/operations";
|
|
2
|
+
import { type McpAdapterOptions } from "./adapter";
|
|
3
|
+
export interface BorrowedStdioOptions<I, O extends Operation = Operation> extends McpAdapterOptions<I, O> {
|
|
4
|
+
/** Trusted, fixed launch identity, never supplied by tool arguments. */
|
|
5
|
+
readonly identity: I;
|
|
6
|
+
readonly maxFrameBytes?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Shared with other dedicated stdio entries; HTTP must not acquire this latch. */
|
|
9
|
+
export declare function acquireStdioOwnership(): () => void;
|
|
10
|
+
/** Borrows the running application; the host alone owns app.stop(). */
|
|
11
|
+
export declare function serveBorrowedStdio<I, O extends Operation = Operation>(options: BorrowedStdioOptions<I, O>): Promise<{
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}>;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Operation } from "@lenso/engine/operations";
|
|
2
|
+
import { type McpAdapterOptions } from "./adapter";
|
|
3
|
+
/** Verified by the host's signature/introspection and current revocation policy, not decoded JSON. */
|
|
4
|
+
export interface McpIdentity {
|
|
5
|
+
readonly subject: string;
|
|
6
|
+
readonly tenant: string;
|
|
7
|
+
readonly issuer: string;
|
|
8
|
+
readonly audience: readonly string[];
|
|
9
|
+
readonly scopes: readonly string[];
|
|
10
|
+
/** Unix seconds. */
|
|
11
|
+
readonly expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface HttpMcpOptions<I extends McpIdentity, O extends Operation = Operation> extends McpAdapterOptions<I, O> {
|
|
14
|
+
/** Canonical public MCP resource URL. Never inferred from forwarded headers. */
|
|
15
|
+
readonly resource: string;
|
|
16
|
+
readonly authorizationServers: readonly string[];
|
|
17
|
+
readonly requiredScopes: readonly string[];
|
|
18
|
+
readonly allowedOrigins: readonly string[];
|
|
19
|
+
/** Must cryptographically verify or introspect the token, including current validity/revocation. */
|
|
20
|
+
readonly verifyToken: (token: string, signal: AbortSignal) => I | Promise<I>;
|
|
21
|
+
readonly maxFrameBytes?: number;
|
|
22
|
+
readonly maxResponseBytes?: number;
|
|
23
|
+
readonly maxHttpRequests?: number;
|
|
24
|
+
readonly maxSessions?: number;
|
|
25
|
+
readonly sessionIdleMs?: number;
|
|
26
|
+
readonly rateLimit?: {
|
|
27
|
+
readonly requests: number;
|
|
28
|
+
readonly windowMs: number;
|
|
29
|
+
readonly maxIdentities: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Owns protocol sessions only. Mount fetch in the host's existing listener;
|
|
34
|
+
* close never stops the borrowed app or changes global logging.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createHttpMcp<I extends McpIdentity, O extends Operation>(options: HttpMcpOptions<I, O>): Promise<{
|
|
37
|
+
fetch(request: Request): Promise<Response>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type OperationBinding } from "@lenso/engine/application";
|
|
2
|
+
export { createMcpAdapter, type McpAdapterOptions, type McpRequestContext } from "./adapter";
|
|
3
|
+
export { createHttpMcp, type HttpMcpOptions, type McpIdentity } from "./http";
|
|
4
|
+
export { serveBorrowedStdio, type BorrowedStdioOptions } from "./borrowed-stdio";
|
|
2
5
|
export interface StdioOptions {
|
|
3
6
|
/** Trusted launch configuration, never supplied by an MCP tool. */
|
|
4
7
|
root: string;
|