@lovable.dev/mcp-js 0.5.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 +21 -0
- package/README.md +386 -0
- package/dist/authorize-DpTEIFvs.d.ts +9 -0
- package/dist/chunk-6DXGZZA4.js +6 -0
- package/dist/chunk-DDF63QWG.js +80 -0
- package/dist/chunk-GLG5RZGE.js +66 -0
- package/dist/chunk-MA5H6PSF.js +46 -0
- package/dist/chunk-QA3FWDUV.js +40 -0
- package/dist/chunk-VD6CS7Y6.js +144 -0
- package/dist/chunk-XEDRJFAR.js +371 -0
- package/dist/index.cjs +271 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +172 -0
- package/dist/protocols/mcp/index.cjs +505 -0
- package/dist/protocols/mcp/index.d.cts +14 -0
- package/dist/protocols/mcp/index.d.ts +14 -0
- package/dist/protocols/mcp/index.js +10 -0
- package/dist/protocols/oauth-metadata.cjs +390 -0
- package/dist/protocols/oauth-metadata.d.cts +8 -0
- package/dist/protocols/oauth-metadata.d.ts +8 -0
- package/dist/protocols/oauth-metadata.js +9 -0
- package/dist/protocols/rest/index.cjs +599 -0
- package/dist/protocols/rest/index.d.cts +11 -0
- package/dist/protocols/rest/index.d.ts +11 -0
- package/dist/protocols/rest/index.js +12 -0
- package/dist/stacks/tanstack/index.cjs +743 -0
- package/dist/stacks/tanstack/index.d.cts +38 -0
- package/dist/stacks/tanstack/index.d.ts +38 -0
- package/dist/stacks/tanstack/index.js +38 -0
- package/dist/stacks/tanstack/vite.cjs +291 -0
- package/dist/stacks/tanstack/vite.d.cts +64 -0
- package/dist/stacks/tanstack/vite.d.ts +64 -0
- package/dist/stacks/tanstack/vite.js +263 -0
- package/dist/types-zMlr1mOK.d.ts +270 -0
- package/package.json +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lovable Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# @lovable.dev/mcp-js
|
|
2
|
+
|
|
3
|
+
Author MCP servers for Lovable apps. Declare tools with `defineTool`, register them with `defineMcp`, and a Vite plugin emits the framework-specific routes at build time (TanStack today, Supabase Edge Functions next).
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// src/lib/mcp/tools/echo.ts
|
|
7
|
+
import { defineTool } from "@lovable.dev/mcp-js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
export default defineTool({
|
|
11
|
+
name: "echo",
|
|
12
|
+
title: "Echo",
|
|
13
|
+
description: "Echo the input text back.",
|
|
14
|
+
inputSchema: { text: z.string().min(1) },
|
|
15
|
+
handler: ({ text }) => ({ content: [{ type: "text", text }] }),
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/lib/mcp/index.ts
|
|
21
|
+
import { defineMcp } from "@lovable.dev/mcp-js";
|
|
22
|
+
import echoTool from "./tools/echo";
|
|
23
|
+
|
|
24
|
+
export default defineMcp({
|
|
25
|
+
name: "my-app-mcp",
|
|
26
|
+
title: "My App MCP",
|
|
27
|
+
version: "0.1.0",
|
|
28
|
+
instructions: "Tools for interacting with My App. Use `echo` to verify connectivity.",
|
|
29
|
+
tools: [echoTool],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// vite.config.ts
|
|
35
|
+
import { defineConfig } from "vite";
|
|
36
|
+
import { mcpPlugin } from "@lovable.dev/mcp-js/stacks/tanstack/vite";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({ plugins: [mcpPlugin()] });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's the whole authoring surface. No imperative server construction, no route handlers, no JSON-RPC envelope, no transport instantiation — all wrapped.
|
|
42
|
+
|
|
43
|
+
## What the plugin emits
|
|
44
|
+
|
|
45
|
+
| URL | File | Purpose |
|
|
46
|
+
| -------------------------------- | ------------------------------------------ | ---------------------------------------- |
|
|
47
|
+
| `POST /mcp` | `src/routes/mcp.ts` | Full MCP streamable-HTTP protocol |
|
|
48
|
+
| `GET /.well-known/oauth-protected-resource` | `src/routes/[.well-known]/oauth-protected-resource.ts` | OAuth protected-resource metadata |
|
|
49
|
+
| `GET /.mcp/list-tools` | `src/routes/[.mcp]/list-tools.ts` | Tool catalog with JSON Schemas |
|
|
50
|
+
| `POST /.mcp/invoke-tool/<tool>` | `src/routes/[.mcp]/invoke-tool/$tool.ts` | REST dispatcher; one handler call per tool |
|
|
51
|
+
|
|
52
|
+
MCP (`POST /mcp`) is the public wire format clients speak directly. REST (`/.mcp/list-tools`, `/.mcp/invoke-tool/<tool>`) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth policy protects them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. CORS and rate limiting still belong at the app host or edge.
|
|
53
|
+
|
|
54
|
+
The OAuth metadata route is emitted by default and returns `404` until OAuth auth is configured. Disable it with `mcpPlugin({ protectedResourceMetadataRoute: false })` only if the app owns `/.well-known/oauth-protected-resource` itself.
|
|
55
|
+
|
|
56
|
+
## OAuth resource-server auth
|
|
57
|
+
|
|
58
|
+
`@lovable.dev/mcp-js` can protect an app-hosted MCP server as an OAuth 2.1 resource server. The package does not implement `/authorize`, `/token`, client registration, refresh tokens, or consent UI; those stay with the authorization server (for today's Supabase-backed Lovable Cloud apps, Supabase Auth). The MCP runtime validates bearer JWTs, publishes RFC 9728 protected-resource metadata, returns `WWW-Authenticate: Bearer ... resource_metadata="..."` challenges, and passes verified claims to tools.
|
|
59
|
+
|
|
60
|
+
Point the issuer at your OAuth/OIDC authorization server and anchor the accepted audience with either `resource` or `acceptedAudiences`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/lib/mcp/index.ts
|
|
64
|
+
import { auth, defineMcp } from "@lovable.dev/mcp-js";
|
|
65
|
+
|
|
66
|
+
export default defineMcp({
|
|
67
|
+
name: "my-app-mcp",
|
|
68
|
+
title: "My App MCP",
|
|
69
|
+
version: "0.1.0",
|
|
70
|
+
instructions: "Tools for interacting with My App.",
|
|
71
|
+
auth: auth.oauth.issuer({
|
|
72
|
+
issuer: "https://auth.example.com",
|
|
73
|
+
resource: "https://my-app.example.com/mcp",
|
|
74
|
+
requiredScopes: ["mcp:tools"],
|
|
75
|
+
}),
|
|
76
|
+
tools: [],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `auth` namespace groups auth families; today it exposes `auth.oauth.issuer(...)`. It is exported from the package root (shown above), the canonical surface for app authors. `issuer` must be HTTPS (except localhost development URLs) and match the token `iss` exactly; the SDK discovers `jwks_uri` from the issuer's OAuth/OIDC metadata. The SDK reads no environment variables — pass values explicitly.
|
|
81
|
+
|
|
82
|
+
### Supabase recipe
|
|
83
|
+
|
|
84
|
+
For a Supabase-backed project, the issuer is the project's `<url>/auth/v1` and the accepted audience is Supabase's project-wide `authenticated` audience:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const supabaseUrl = process.env.SUPABASE_URL!.replace(/\/+$/, "");
|
|
88
|
+
|
|
89
|
+
auth: auth.oauth.issuer({
|
|
90
|
+
issuer: `${supabaseUrl}/auth/v1`,
|
|
91
|
+
acceptedAudiences: "authenticated",
|
|
92
|
+
}),
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Supabase auth is **project-scoped**: Supabase access tokens use `aud: "authenticated"`, so the same delegated token verifies against every MCP server in that project. The binding is "an OAuth client of the project," not "this MCP server." Use `ctx.getClientId()`, `ctx.getUserId()`, and `ctx.getClaims()` for handler-level app/business checks — never authorize on the audience — and pass `ctx.getToken()` through to Supabase so RLS evaluates the token itself. Supabase tokens do not carry OAuth scopes today, so leave `requiredScopes` unset.
|
|
96
|
+
|
|
97
|
+
To skip metadata discovery entirely, pin `jwksUri` to the issuer's JWKS endpoint. Verification then makes zero discovery probes; key rotation still works because the JWKS is fetched (and refreshed) on demand:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
auth: auth.oauth.issuer({
|
|
101
|
+
issuer: `${supabaseUrl}/auth/v1`,
|
|
102
|
+
acceptedAudiences: "authenticated",
|
|
103
|
+
jwksUri: `${supabaseUrl}/auth/v1/.well-known/jwks.json`,
|
|
104
|
+
}),
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For a direct Supabase project, the relevant URLs look like this:
|
|
108
|
+
|
|
109
|
+
| Supabase URL | Used by |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configuration` | SDK: discovers `issuer` and `jwks_uri` |
|
|
112
|
+
| `https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json` | SDK: verifies bearer JWT signatures |
|
|
113
|
+
| `https://<project-ref>.supabase.co/auth/v1/oauth/authorize` | OAuth clients: start user authorization |
|
|
114
|
+
| `https://<project-ref>.supabase.co/auth/v1/oauth/token` | OAuth clients: exchange/refresh tokens |
|
|
115
|
+
|
|
116
|
+
The SDK only uses discovery + JWKS. It never calls the authorization or token endpoints.
|
|
117
|
+
|
|
118
|
+
Tool handlers receive their input args plus a `ToolContext` as the second
|
|
119
|
+
argument. The SDK constructs one per request from the verified token and passes
|
|
120
|
+
it to the handler; helpers that need the auth take a `ToolContext` parameter, so
|
|
121
|
+
the dependency is explicit in their signatures.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import type { ToolContext } from "@lovable.dev/mcp-js";
|
|
125
|
+
|
|
126
|
+
handler: async ({ title }, ctx) => {
|
|
127
|
+
const userId = ctx.getUserId(); // token `sub`, or undefined when unauthenticated
|
|
128
|
+
// Apply app/business permissions here using ctx.getClaims() / ctx.getClientId().
|
|
129
|
+
// For Supabase RLS, pass ctx.getToken() through to Supabase.
|
|
130
|
+
return { content: [{ type: "text", text: `user=${userId}` }] };
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| `ToolContext` method | Returns |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `isAuthenticated()` | `true` when the call carries a verified auth context. |
|
|
137
|
+
| `getUserId()` | The token `sub` (subject) — the user id. |
|
|
138
|
+
| `getUserEmail()` | Token `email` when present. |
|
|
139
|
+
| `getClientId()` | OAuth `client_id`, falling back to the OIDC `azp` (authorized party) — the client binding handle, useful for authorization. |
|
|
140
|
+
| `getScopes()` | The parsed OAuth `scope`s. |
|
|
141
|
+
| `getIssuer()` | The verified token issuer. |
|
|
142
|
+
| `getClaims()` | The full verified JWT claims — for app/business authorization on issuer-specific claims with no dedicated accessor. |
|
|
143
|
+
| `getToken()` | The raw bearer — pass to downstream APIs (e.g. Supabase RLS); never return or log it. |
|
|
144
|
+
|
|
145
|
+
Each returns `undefined` when the server is unauthenticated. `ToolContext` is a
|
|
146
|
+
plain object passed by value — there is no ambient store, no `AsyncLocalStorage`,
|
|
147
|
+
and no `node:async_hooks` dependency, so it works on any runtime and is safe
|
|
148
|
+
under concurrency by construction. A handler that hands `ctx` to deferred work (a
|
|
149
|
+
detached timer, an un-awaited promise) keeps full access to it.
|
|
150
|
+
|
|
151
|
+
**Security contract for the bearer token.** `getToken()` is the only way to read
|
|
152
|
+
the raw token. `ToolContext` holds the verified context in a private field, so the
|
|
153
|
+
credential can't be read off the instance via `JSON.stringify`, object spread, or
|
|
154
|
+
`structuredContent` — only the accessors above expose anything, and `getToken()` is
|
|
155
|
+
the lone path to the bearer. Pass `getToken()` only into outbound `fetch`/client
|
|
156
|
+
construction (e.g. calling Supabase on the user's behalf); never return it from a
|
|
157
|
+
tool or write it to logs. A leaked token stays valid at the authorization server
|
|
158
|
+
until it expires.
|
|
159
|
+
|
|
160
|
+
Use an OAuth access token from the configured authorization server for MCP calls. Plain app-session JWTs, such as Supabase tokens from `signInWithPassword`, carry neither `client_id` nor `azp`; the SDK rejects them by default so copied browser sessions do not pass as delegated OAuth client tokens. Set `requireOAuthClientClaim: false` only when intentionally accepting those session tokens and relying on app checks plus downstream RLS via the forwarded bearer token.
|
|
161
|
+
|
|
162
|
+
`auth.oauth.issuer(...)` fields:
|
|
163
|
+
|
|
164
|
+
| Field | Meaning |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `issuer` | OAuth/OIDC issuer URL. Must be HTTPS except localhost development URLs and must match the token `iss` exactly. Required. |
|
|
167
|
+
| `resource` | Canonical protected-resource URL published in metadata. It also anchors the accepted audience unless `acceptedAudiences` is set. When omitted, the published resource is derived from the request origin plus the stack adapter's `resourcePath` and you must set `acceptedAudiences`; the metadata handler throws at construction if neither `resource` nor `resourcePath` is set. |
|
|
168
|
+
| `acceptedAudiences` | Accepted JWT audience(s). Defaults to `resource` when that is set. One of `resource` or `acceptedAudiences` is required. |
|
|
169
|
+
| `jwksUri` | JWKS URL override. If omitted, the runtime discovers `jwks_uri` from OAuth/OIDC metadata; setting it skips discovery entirely. |
|
|
170
|
+
| `requiredScopes` | Required scopes. Advertised in metadata/challenges and enforced against the token `scope`. |
|
|
171
|
+
| `requireOAuthClientClaim` | Defaults to `true`; rejects tokens carrying neither `client_id` nor the OIDC `azp` (authorized party) so copied app-session JWTs do not pass as OAuth client tokens. Supabase OAuth 2.1 server tokens carry `client_id` and issuers like Google/Keycloak/Entra use `azp`, so keep the default; only legacy `signInWithPassword` session tokens lack both and need `false`. |
|
|
172
|
+
| `algorithms` | Accepted JWT signing algorithms. Defaults to asymmetric algorithms (`RS*`, `ES*`, `EdDSA`); set explicitly only for a narrower policy. |
|
|
173
|
+
| `clockToleranceSeconds` | Allowed JWT clock skew for `exp`/`nbf` checks, in seconds. Defaults to `30`; max `300`. |
|
|
174
|
+
| `resourceName`, `resourceDocumentation` | Optional metadata fields for MCP clients and humans. `resourceName` defaults to the MCP server `title`. |
|
|
175
|
+
| `protectedResourceMetadataUrl` | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 `WWW-Authenticate: resource_metadata` challenge and stops generating its own metadata — the `/.well-known/oauth-protected-resource` handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass `protectedResourceMetadataRoute: false` to the Vite plugin so the now-unused route isn't emitted. |
|
|
176
|
+
|
|
177
|
+
Protected-resource metadata is derived from the same config. When `resource` is omitted, the published resource is the incoming request origin plus the MCP route path the stack adapter supplies as `resourcePath` (for example, `https://my-app.lovable.app/mcp`); `authorization_servers` is the configured issuer, and `resource_name` is `defineMcp({ title })` unless `resourceName` overrides it. Because the metadata endpoint is served from `/.well-known/...`, it can't infer the resource from its own path — the handler throws at construction when neither `resource` nor `resourcePath` is set (the generated TanStack routes always pass `resourcePath`). For Supabase auth, this metadata still names the MCP resource even though Supabase JWTs use the project audience `"authenticated"`.
|
|
178
|
+
|
|
179
|
+
Stack adapters must pass the public MCP route as `resourcePath` when they also expose REST companion routes. The REST companion handlers (`createInvokeToolHandler`, `createListToolsHandler`) throw at construction unless `resource` or `resourcePath` is set, so `principal.resource` binds to `/mcp` rather than leaking the internal `/.mcp/*` RPC path it was reached through. The generated TanStack routes already pass it.
|
|
180
|
+
|
|
181
|
+
The request-derived `resource` default trusts the incoming `Host` to name this server's origin. Because a config always sets `acceptedAudiences` (Supabase: `"authenticated"`) or `resource`, this default only names the advertised protected-resource metadata, never the accepted JWT audience. A platform that terminates TLS and sets `Host` from the verified domain (Lovable Cloud) makes this safe. Deployments fronted by a proxy that forwards an attacker-controlled `Host` should pin `resource` to the canonical URL so a spoofed host cannot shift the advertised metadata URL.
|
|
182
|
+
|
|
183
|
+
Use `auth.oauth.issuer(...)`, set `resource` or `acceptedAudiences` to anchor the accepted audience, and optionally set `jwksUri` and `requiredScopes`. For Supabase project auth, set `acceptedAudiences: "authenticated"`, keep app/business checks in app code, and forward `ctx.getToken()` to Supabase for RLS-backed data access.
|
|
184
|
+
|
|
185
|
+
## Subpath exports
|
|
186
|
+
|
|
187
|
+
| Subpath | Contents |
|
|
188
|
+
| --------------------------------------- | ----------------------------------------------------------------------- |
|
|
189
|
+
| `@lovable.dev/mcp-js` | `defineTool`, `defineMcp`, public types |
|
|
190
|
+
| `@lovable.dev/mcp-js/protocols/mcp` | `createMcpProtocolHandler` — Web-Standard MCP-over-HTTP |
|
|
191
|
+
| `@lovable.dev/mcp-js/protocols/oauth-metadata` | `createOAuthProtectedResourceMetadataHandler` (the `auth` namespace lives at the root) |
|
|
192
|
+
| `@lovable.dev/mcp-js/protocols/rest` | `createListToolsHandler`, `createInvokeToolHandler` — Web-Standard |
|
|
193
|
+
| `@lovable.dev/mcp-js/stacks/tanstack` | TanStack-route-ctx adapters (`createTanStack*Handler`) |
|
|
194
|
+
| `@lovable.dev/mcp-js/stacks/tanstack/vite` | The Vite plugin |
|
|
195
|
+
|
|
196
|
+
The folders carry distinct meaning:
|
|
197
|
+
|
|
198
|
+
- **`protocols/`** is the wire layer — one folder per externally observable HTTP surface. `protocols/mcp` is the public MCP-over-HTTP surface; `protocols/oauth-metadata` serves the RFC 9728 protected-resource metadata required to bootstrap protected MCP clients; `protocols/rest` is internal RPC for the upstream MCP proxy (see "What the plugin emits"). Every backend wires both MCP and REST, plus the metadata endpoint when OAuth is configured.
|
|
199
|
+
- **`auth/`** (unpublished) is the cross-cutting OAuth middleware — bearer verification, issuer/JWKS discovery, the `auth.oauth.*` config namespace. It depends only on `core/`; both `protocols/mcp` and `protocols/rest` depend on it for the shared bearer gate, so it isn't a wire-format peer of `mcp`/`rest` and doesn't live under `protocols/`.
|
|
200
|
+
- **`stacks/`** is the framework integration: TanStack today; future entries (Supabase Edge Functions, classic Vite, …) live next to it under the same parent.
|
|
201
|
+
|
|
202
|
+
Generated route files import from `@lovable.dev/mcp-js/stacks/tanstack`. End users only need the root import. Future stacks (Supabase Edge, classic Vite, …) land under `stacks/` as siblings, forwarding to `protocols/{mcp,oauth-metadata,rest}` directly — protocol logic stays shared, only ctx unwrapping differs.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Design decisions
|
|
207
|
+
|
|
208
|
+
### 1. Explicit `defineMcp({ tools })` over file-based discovery
|
|
209
|
+
|
|
210
|
+
`defineTool` is a typed identity function. The user imports tools explicitly into `defineMcp({ tools: [...] })`. The Vite plugin scans nothing — it emits routes that read the user's array at runtime.
|
|
211
|
+
|
|
212
|
+
Reasons:
|
|
213
|
+
|
|
214
|
+
- **Implicit registration drifts silently.** Tool name + file name can disagree; duplicates only surface at runtime in `registerTool`.
|
|
215
|
+
- **Refactoring is hostile.** Renaming a tool means renaming its file. Sub-directories require the plugin to recurse, and there's no clean convention for "implementation vs. helper" files.
|
|
216
|
+
- **PR review can't grep the surface.** A reader can mentally check `tools: [echoTool, addTool]`; they cannot mentally check a directory scan.
|
|
217
|
+
|
|
218
|
+
### 2. Build-time route emission via a Vite plugin
|
|
219
|
+
|
|
220
|
+
TanStack's file-based router needs a route file on disk to register a URL. If we asked users to author the route file themselves, they'd own seven lines of plumbing that has to stay correct as the SDK's transport semantics evolve. Generating it lets us pin the wiring as a versioned package artifact — when the MCP SDK changes, the plugin emits a new template and every user picks it up on rebuild.
|
|
221
|
+
|
|
222
|
+
This is the same insight that drives shipping an SDK instead of having the agent author the MCP runtime: bug fixes ship to all apps on the next build, not per-app.
|
|
223
|
+
|
|
224
|
+
### 3. The `[.mcp]` URL prefix uses bracket escaping
|
|
225
|
+
|
|
226
|
+
TanStack's file routing maps filenames to URLs and filters dot-prefixed entries as hidden — both directories (`src/routes/.mcp/...`) and flat files (`.mcp.list-tools.ts`). The escape is bracket-quoted literal segments: `[.mcp]/list-tools.ts` maps to `/.mcp/list-tools` and shows up in the generated route tree. The plugin emits at `src/routes/[.mcp]/list-tools.ts` and `src/routes/[.mcp]/invoke-tool/$tool.ts`.
|
|
227
|
+
|
|
228
|
+
### 4. `invoke-tool/<tool>` instead of `<tool>` directly
|
|
229
|
+
|
|
230
|
+
The `invoke-tool/` segment scopes the user-defined tool namespace, so future endpoints under `/.mcp/` (`list-tools`, `health`, `audit-log`, anything else added) can't collide with a user-defined tool named the same thing. Cheap upfront, breaking to add later.
|
|
231
|
+
|
|
232
|
+
### 5. Dynamic tool resolution at runtime
|
|
233
|
+
|
|
234
|
+
The dispatcher resolves the tool name against the live `mcp.tools` array on every request. From the caller's perspective `POST /.mcp/invoke-tool/echo` and `POST /.mcp/invoke-tool/add` look like separate endpoints; from the author's perspective the source of truth is one array. **Tool resolution is a per-request operation, not a per-build operation.**
|
|
235
|
+
|
|
236
|
+
This is architectural foundation, not convenience. Resolution stays inside app code so it can grow:
|
|
237
|
+
|
|
238
|
+
- **Per-user / per-scope tool visibility.** The lookup that maps `params.tool` to a handler can run *inside* app code, with access to the request's authenticated identity. An admin user might see `purge_workspace`; a regular user wouldn't see it in `list-tools` and would get a 404 from `invoke-tool`. Plan-tier gating, beta cohorts, feature flags, workspace roles — all want runtime context to decide what to expose.
|
|
239
|
+
- **The build doesn't have user context.** Emitting per-tool files at build time would lock the tool catalog to the deploy. Dynamic resolution leaves that decision to the request lifecycle.
|
|
240
|
+
- **The MCP and REST surfaces stay in sync automatically.** `tools/list` (over MCP) and `GET /.mcp/list-tools` (over REST) both read the same array. When per-user resolution lands, both surfaces filter through the same hook.
|
|
241
|
+
|
|
242
|
+
`list-tools` stays a separate handler (not auto-injected into the dispatcher) because it's naturally a `GET` — distinct HTTP semantics from `invoke-tool`'s `POST`. The per-user filter will hook into both, but the HTTP shape stays clean.
|
|
243
|
+
|
|
244
|
+
**Planned:** delegate the per-request tool-resolution step to app code via a hook (something like `resolveTools(ctx)` returning the subset of `mcp.tools` the caller can see). Until then, every authenticated caller sees the full array.
|
|
245
|
+
|
|
246
|
+
### 6. Type-erased `AnyToolDefinition` at the array boundary
|
|
247
|
+
|
|
248
|
+
`ToolDefinition<TInput>` is generic — the handler's args are inferred from `inputSchema`:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
handler: TInput extends ZodRawShape
|
|
252
|
+
? (args: ShapeOutput<TInput>) => ToolHandlerResult | Promise<...>
|
|
253
|
+
: () => ToolHandlerResult | Promise<...>;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This works per-tool at the `defineTool` call site. It breaks at the `defineMcp({ tools: [...] })` boundary because of **function-parameter contravariance**: a heterogeneous array of `ToolDefinition<{a}>` and `ToolDefinition<{b, c}>` can't collapse to one generic without rejecting at least one entry.
|
|
257
|
+
|
|
258
|
+
`AnyToolDefinition` is the non-generic version used only at the array boundary, with `handler: (args: any) => ...`. `any` is bivariant in TypeScript, so any concrete handler assigns. Per-arg typing still happens inside `defineTool` — the only place it materially matters.
|
|
259
|
+
|
|
260
|
+
### 7. Title, description, and instructions are required
|
|
261
|
+
|
|
262
|
+
The MCP spec promoted `title` to a top-level field on `Tool` and `Server` in 2025-06-18+; we require it. We also require `description` on tools and `instructions` on servers because:
|
|
263
|
+
|
|
264
|
+
- Both are read by LLMs as context for tool selection. A missing/templated description or instructions string produces agents that call the wrong tool or call none at all.
|
|
265
|
+
- Optional prose fields encourage drift over time. Required-at-the-type-level enforces "intentional surface area" at PR-review time.
|
|
266
|
+
- `instructions: ""` is the documented opt-out for servers that genuinely have nothing supplementary to say (single-purpose tools whose descriptions speak for themselves). Empty-string is intentional rather than accidental.
|
|
267
|
+
|
|
268
|
+
The MCP spec's legacy `annotations.title` is omitted from our `ToolAnnotations` type — use the top-level `title` instead. Two locations for the same field encourage drift.
|
|
269
|
+
|
|
270
|
+
### 8. Decoupled type surface from `@modelcontextprotocol/sdk`
|
|
271
|
+
|
|
272
|
+
Two reasons:
|
|
273
|
+
|
|
274
|
+
1. **`.d.ts` stability across SDK bumps.** The SDK's published type tree (`@modelcontextprotocol/sdk/types.js`) and its zod-compat shape generics have shipped breaking type-only changes between minor versions. Owning our own type re-declarations lets us bump the SDK's runtime without forcing every downstream consumer to re-type-check.
|
|
275
|
+
2. **Optionality on the SDK itself.** If we ever swap or drop `@modelcontextprotocol/sdk` — different transport, in-house protocol implementation, a fork — consumers' code is the contract that matters, and it's typed against this package's surface, not the SDK's. The runtime hand-off can change without rippling through user code.
|
|
276
|
+
|
|
277
|
+
The published `.d.ts` files import only from `zod` and `vite`. Three SDK concerns were re-implemented (or re-typed) locally:
|
|
278
|
+
|
|
279
|
+
- **Content blocks and `ToolAnnotations`** — re-declared in `core/types.ts` to match the MCP wire format without depending on `@modelcontextprotocol/sdk/types.js`. The runtime still serializes via the SDK; only the type tree is owned.
|
|
280
|
+
- **`ZodRawShape`, `ZodType`, `infer`** — sourced directly from `zod` (a peer dep). zod v3 and v4 both export all three at the top level; `ZodType` is non-deprecated in both versions with safe generic defaults. We re-export them from `core/types.ts` as `ZodRawShape` / `ZodSchema` / `ShapeOutput` (`ZodSchema` is a no-generic alias for `ZodType`). No dependency on `@modelcontextprotocol/sdk/server/zod-compat.js` types.
|
|
281
|
+
- **`ContentBlock` union** — `TextContent | ImageContent | AudioContent | EmbeddedResource | ResourceLink`, structurally matching MCP's wire format. Consumers can type custom content (`return { content: [<ImageContent>, <TextContent>] }`) without referencing the SDK.
|
|
282
|
+
|
|
283
|
+
The **runtime** still hands off to the MCP SDK — we still `import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"` and call `registerTool`, and we still use `objectFromShape` + `safeParseAsync` + `toJsonSchemaCompat` from the SDK's zod-compat module for REST input validation and JSON-Schema serialization. That's a runtime dependency, not a type-surface dependency.
|
|
284
|
+
|
|
285
|
+
### 9. Stale-route cleanup on every regen
|
|
286
|
+
|
|
287
|
+
The `GENERATED_BANNER` is the SDK's ownership token. Cleanup walks the whole `routesDir`, and any `.ts` file that carries the banner but is no longer in the current emit set gets unlinked. Catches two real cases:
|
|
288
|
+
|
|
289
|
+
- **Plugin version upgrades** that move or rename a route. Without cleanup, a stale file keeps importing a symbol the package no longer exports and breaks the build.
|
|
290
|
+
- **MCP entry removal.** Deleting `lib/mcp/index.ts` triggers the cleanup pass with an empty expected set; every banner-tagged file gets unlinked. Without this, the orphans still imported the deleted module and broke the build until removed by hand.
|
|
291
|
+
|
|
292
|
+
Two consequences for user-authored files:
|
|
293
|
+
|
|
294
|
+
- **Without the banner** (e.g., your own `src/routes/widgets.ts`): left alone. The banner is the only ownership signal, and it's 200+ idiosyncratic characters, so accidental collision is implausible.
|
|
295
|
+
- **With the banner, anywhere under `routesDir`**: treated as SDK-owned and reclaimed if it isn't in the current emit set. To take ownership of such a file, delete the banner line — the plugin then leaves it alone. Conversely, a hand-authored file *at* an emit path *without* the banner makes `writeIfChanged` refuse with a build-time error — `refusing to overwrite user-authored route at <path>. Delete it or move it first.`
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Layout
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
src/
|
|
303
|
+
index.ts # public entrypoint: defineTool/defineMcp, auth, ToolContext, public types
|
|
304
|
+
core/ # framework-agnostic kernel; not a published subpath
|
|
305
|
+
define.ts # defineTool, defineMcp
|
|
306
|
+
types.ts # types (zod-sourced shape types, MCP wire types)
|
|
307
|
+
http.ts # Web-Standard Response helpers
|
|
308
|
+
url.ts # URL parsing/validation
|
|
309
|
+
validation.ts # config assertions
|
|
310
|
+
promise.ts # cachedPromise (resolved-value cache)
|
|
311
|
+
auth/ # cross-cutting OAuth middleware (unpublished); depends only on core/
|
|
312
|
+
config.ts # auth namespace: auth.oauth.issuer
|
|
313
|
+
authorize.ts # request authorizer, bearer parsing, challenges
|
|
314
|
+
verifier.ts # JWT verification and auth context construction
|
|
315
|
+
context.ts # ToolContext (verified auth passed to tool handlers)
|
|
316
|
+
claims.ts # JWT claim accessors (scope/string helpers)
|
|
317
|
+
discovery.ts # OAuth/OIDC metadata and JWKS URI discovery
|
|
318
|
+
resource.ts # protected-resource URL resolution
|
|
319
|
+
metadata-path.ts # shared RFC 9728 metadata path constant
|
|
320
|
+
types.ts # auth config + context types
|
|
321
|
+
protocols/
|
|
322
|
+
oauth-metadata.ts # createOAuthProtectedResourceMetadataHandler (RFC 9728 wire surface)
|
|
323
|
+
mcp/
|
|
324
|
+
protocol.ts # createMcpProtocolHandler (Web-Standard)
|
|
325
|
+
index.ts # barrel
|
|
326
|
+
rest/
|
|
327
|
+
list-tools.ts # createListToolsHandler (Web-Standard, GET)
|
|
328
|
+
invoke-tool.ts # createInvokeToolHandler (Web-Standard, POST)
|
|
329
|
+
index.ts # barrel
|
|
330
|
+
stacks/
|
|
331
|
+
tanstack/
|
|
332
|
+
handlers.ts # TanStack route-ctx adapters
|
|
333
|
+
vite.ts # mcpPlugin (route emission + stale-file cleanup)
|
|
334
|
+
index.ts # barrel
|
|
335
|
+
tests/
|
|
336
|
+
core/{define,promise,url}.test.ts
|
|
337
|
+
auth/{claims,config,context,oauth}.test.ts # OAuth config + bearer verification
|
|
338
|
+
protocols/
|
|
339
|
+
mcp/protocol.test.ts
|
|
340
|
+
rest/{list-tools,invoke-tool}.test.ts
|
|
341
|
+
parity.test.ts # REST↔MCP equivalence contract
|
|
342
|
+
stacks/
|
|
343
|
+
tanstack/{handlers,vite}.test.ts
|
|
344
|
+
integration/ # boots an example app, hits live HTTP
|
|
345
|
+
global-setup.ts # spawns example/tanstack on :8080
|
|
346
|
+
tools.test.ts # non-OAuth example
|
|
347
|
+
oauth.test.ts # self-contained: mock issuer + OAuth example
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Scripts
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
pnpm format # oxfmt --write src/ tests/
|
|
354
|
+
pnpm format:check # oxfmt --check src/ tests/
|
|
355
|
+
pnpm lint # format:check + oxlint (one command, both surfaces)
|
|
356
|
+
pnpm lint:fix # format + oxlint --fix
|
|
357
|
+
pnpm typecheck # tsgo --noEmit
|
|
358
|
+
pnpm test # vitest run --project unit (fast, hermetic)
|
|
359
|
+
pnpm test:watch # vitest --project unit
|
|
360
|
+
pnpm test:integration # build + install example + vitest run --project integration
|
|
361
|
+
pnpm test:integration:oauth # build + install OAuth example + vitest run --project integration-oauth
|
|
362
|
+
pnpm build # tsup → dist/{cjs,esm}/{index,protocols/*,stacks/*}
|
|
363
|
+
pnpm pack:local # build + npm pack into /tmp/lovable.dev-mcp-js-<version>.tgz
|
|
364
|
+
pnpm example:install # install example/tanstack deps (called by test:integration)
|
|
365
|
+
pnpm example:oauth:install # install example/tanstack-supabase-oauth deps
|
|
366
|
+
pnpm example:oauth:build # production-build the OAuth example
|
|
367
|
+
pnpm example:oauth:smoke # smoke a running OAuth example, optionally with MCP_ACCESS_TOKEN
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`lint` checks both formatting (oxfmt) and lint rules (oxlint) in one pass — the format check fails fast before the lint pass so you see the smaller diff first. `prepublishOnly` chains `clean → typecheck → test → build`; CI enforces `lint` separately.
|
|
371
|
+
|
|
372
|
+
**Integration tests** live under `tests/integration/` and run against a live HTTP server. `pnpm test:integration` builds the SDK, installs `example/tanstack`, boots its dev server on port 8080 via a Vitest `globalSetup`, runs the suite, and tears the server down. If the server is already running at `MCP_BASE_URL` (default `http://localhost:8080`), the setup reuses it — useful when iterating on the suite. See `CHANGELOG.md` for version history.
|
|
373
|
+
|
|
374
|
+
**OAuth integration test** (`tests/integration/oauth.test.ts`) is hermetic: `pnpm test:integration:oauth` builds + installs `example/tanstack-supabase-oauth`, then a self-contained `beforeAll` starts a mock RS256 issuer (JWKS + Supabase userinfo/PostgREST stubs), boots the example pointed at it on port 8081, signs a local test JWT, and asserts metadata, bearer challenges, and authorized REST + MCP calls. It runs in its own `integration-oauth` Vitest project (no shared `globalSetup`).
|
|
375
|
+
|
|
376
|
+
**OAuth local loop** lives under `example/tanstack-supabase-oauth`. `pnpm example:oauth:dev` uses that example's checked-in `.env`, which points at the shared Lovable/Supabase test project; `pnpm example:oauth:smoke` verifies metadata/challenge behavior against a running example and, with `MCP_ACCESS_TOKEN`, authenticated calls.
|
|
377
|
+
|
|
378
|
+
**Local-pack workflow** (for testing the SDK against a downstream consumer without publishing):
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
pnpm pack:local # → /tmp/lovable.dev-mcp-js-<version>.tgz
|
|
382
|
+
cd path/to/your/app
|
|
383
|
+
bun add /tmp/lovable.dev-mcp-js-<version>.tgz # or `pnpm add`, `npm i`
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Note: bun caches tarballs by absolute path. If you re-run `pack:local` without bumping the version, bun will re-extract from cache rather than picking up the new bytes. Bump `version` in `package.json` between iterations.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface McpRuntimeOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Public MCP protocol path. REST companion handlers pass this so OAuth
|
|
4
|
+
* resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
|
|
5
|
+
*/
|
|
6
|
+
resourcePath?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { McpRuntimeOptions as M };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSON_HEADERS,
|
|
3
|
+
getOAuthRuntime,
|
|
4
|
+
headResponse,
|
|
5
|
+
methodNotAllowed,
|
|
6
|
+
oauthConfigurationErrorResponse,
|
|
7
|
+
resolveProtectedResource
|
|
8
|
+
} from "./chunk-XEDRJFAR.js";
|
|
9
|
+
|
|
10
|
+
// src/protocols/oauth-metadata.ts
|
|
11
|
+
var CORS_ORIGIN = { "Access-Control-Allow-Origin": "*" };
|
|
12
|
+
function withCors(response) {
|
|
13
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
function notFound() {
|
|
17
|
+
return new Response(JSON.stringify({ error: "not found" }), {
|
|
18
|
+
status: 404,
|
|
19
|
+
// `no-store` so a 404 (OAuth unconfigured) isn't heuristically cached and
|
|
20
|
+
// then served past a later deploy that enables OAuth and starts returning 200.
|
|
21
|
+
headers: { ...JSON_HEADERS, ...CORS_ORIGIN, "Cache-Control": "no-store" }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function buildProtectedResourceMetadata(mcp, auth, request, options, discovery) {
|
|
25
|
+
const issuer = await discovery.resolveIssuer();
|
|
26
|
+
const body = {
|
|
27
|
+
resource: resolveProtectedResource(auth, request, options),
|
|
28
|
+
authorization_servers: [issuer],
|
|
29
|
+
bearer_methods_supported: ["header"],
|
|
30
|
+
resource_name: auth.resourceName ?? mcp.title,
|
|
31
|
+
resource_documentation: auth.resourceDocumentation
|
|
32
|
+
};
|
|
33
|
+
if (auth.requiredScopes && auth.requiredScopes.length > 0)
|
|
34
|
+
body.scopes_supported = auth.requiredScopes;
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
38
|
+
const runtime = getOAuthRuntime(mcp, options);
|
|
39
|
+
if (runtime.kind === "configured" && runtime.auth.protectedResourceMetadataUrl === void 0 && runtime.auth.resource === void 0 && runtime.options.resourcePath === void 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`@lovable.dev/mcp-js: auth.resource or a resourcePath is required so the protected-resource metadata doesn't advertise the well-known URL as the resource`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return async (request) => {
|
|
45
|
+
if (runtime.kind !== "configured" || runtime.auth.protectedResourceMetadataUrl !== void 0)
|
|
46
|
+
return notFound();
|
|
47
|
+
if (request.method === "OPTIONS") {
|
|
48
|
+
return new Response(null, {
|
|
49
|
+
status: 204,
|
|
50
|
+
headers: { ...CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS" }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
54
|
+
return withCors(methodNotAllowed("GET, HEAD, OPTIONS"));
|
|
55
|
+
const headers = {
|
|
56
|
+
...JSON_HEADERS,
|
|
57
|
+
...CORS_ORIGIN,
|
|
58
|
+
"Cache-Control": "public, max-age=300",
|
|
59
|
+
Vary: "Host"
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
const metadata = await buildProtectedResourceMetadata(
|
|
63
|
+
mcp,
|
|
64
|
+
runtime.auth,
|
|
65
|
+
request,
|
|
66
|
+
runtime.options,
|
|
67
|
+
runtime.discovery
|
|
68
|
+
);
|
|
69
|
+
const response = Response.json(metadata, { headers });
|
|
70
|
+
return request.method === "HEAD" ? headResponse(response) : response;
|
|
71
|
+
} catch {
|
|
72
|
+
const response = withCors(oauthConfigurationErrorResponse());
|
|
73
|
+
return request.method === "HEAD" ? headResponse(response) : response;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
createOAuthProtectedResourceMetadataHandler
|
|
80
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolContext
|
|
3
|
+
} from "./chunk-MA5H6PSF.js";
|
|
4
|
+
import {
|
|
5
|
+
createRequestAuthorizer
|
|
6
|
+
} from "./chunk-XEDRJFAR.js";
|
|
7
|
+
|
|
8
|
+
// src/protocols/mcp/protocol.ts
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
11
|
+
function adaptToolToSdkCallback(tool, auth) {
|
|
12
|
+
return async (first) => {
|
|
13
|
+
const args = tool.inputSchema ? first ?? {} : {};
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await tool.handler(args, new ToolContext(auth));
|
|
17
|
+
} catch {
|
|
18
|
+
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
if (result == null) {
|
|
21
|
+
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function createMcpProtocolHandler(mcp, options = {}) {
|
|
27
|
+
const authorizer = createRequestAuthorizer(mcp, options);
|
|
28
|
+
return async (request) => {
|
|
29
|
+
const authResult = await authorizer.authorize(request);
|
|
30
|
+
if (!authResult.ok)
|
|
31
|
+
return authResult.response;
|
|
32
|
+
try {
|
|
33
|
+
const server = new McpServer(
|
|
34
|
+
{ name: mcp.name, version: mcp.version, title: mcp.title },
|
|
35
|
+
{ instructions: mcp.instructions }
|
|
36
|
+
);
|
|
37
|
+
for (const tool of mcp.tools) {
|
|
38
|
+
server.registerTool(
|
|
39
|
+
tool.name,
|
|
40
|
+
{
|
|
41
|
+
title: tool.title,
|
|
42
|
+
description: tool.description,
|
|
43
|
+
inputSchema: tool.inputSchema,
|
|
44
|
+
outputSchema: tool.outputSchema,
|
|
45
|
+
annotations: tool.annotations
|
|
46
|
+
},
|
|
47
|
+
adaptToolToSdkCallback(tool, authResult.auth)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
51
|
+
sessionIdGenerator: void 0
|
|
52
|
+
});
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
return await transport.handleRequest(request);
|
|
55
|
+
} catch {
|
|
56
|
+
return Response.json(
|
|
57
|
+
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
58
|
+
{ status: 500 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
createMcpProtocolHandler
|
|
66
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/auth/context.ts
|
|
2
|
+
var ToolContext = class {
|
|
3
|
+
#auth;
|
|
4
|
+
constructor(auth) {
|
|
5
|
+
this.#auth = auth;
|
|
6
|
+
}
|
|
7
|
+
/** Whether the in-flight tool call carries a verified auth context. */
|
|
8
|
+
isAuthenticated() {
|
|
9
|
+
return this.#auth !== void 0;
|
|
10
|
+
}
|
|
11
|
+
/** The verified bearer token, or `undefined` when unauthenticated. Pass it to downstream APIs; never return or log it. */
|
|
12
|
+
getToken() {
|
|
13
|
+
return this.#auth?.bearer.token;
|
|
14
|
+
}
|
|
15
|
+
/** The verified user id (the token `sub`), or `undefined`. */
|
|
16
|
+
getUserId() {
|
|
17
|
+
return this.#auth?.principal.sub;
|
|
18
|
+
}
|
|
19
|
+
/** The verified user email, or `undefined` when absent. */
|
|
20
|
+
getUserEmail() {
|
|
21
|
+
return this.#auth?.principal.email;
|
|
22
|
+
}
|
|
23
|
+
/** The verified OAuth `client_id`, or `undefined`. */
|
|
24
|
+
getClientId() {
|
|
25
|
+
return this.#auth?.principal.clientId;
|
|
26
|
+
}
|
|
27
|
+
/** The verified OAuth scopes, or `undefined` when unauthenticated. */
|
|
28
|
+
getScopes() {
|
|
29
|
+
return this.#auth?.principal.scopes;
|
|
30
|
+
}
|
|
31
|
+
/** The verified token issuer, or `undefined`. */
|
|
32
|
+
getIssuer() {
|
|
33
|
+
return this.#auth?.principal.issuer;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The full verified JWT claims, or `undefined`. Use this for app/business
|
|
37
|
+
* authorization on issuer-specific claims that have no dedicated accessor.
|
|
38
|
+
*/
|
|
39
|
+
getClaims() {
|
|
40
|
+
return this.#auth?.principal.claims;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
ToolContext
|
|
46
|
+
};
|