@vercel/connect 2.2.0 → 2.3.1
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 +59 -2
- package/dist/ai-sdk/index.d.ts +7 -5
- package/dist/ai-sdk/index.js +7 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/connect-auth-provider.d.ts +217 -7
- package/dist/mcp/connect-auth-provider.js +197 -32
- package/dist/mcp/index.d.ts +7 -4
- package/dist/mcp/index.js +7 -4
- package/dist/tanstack-ai/index.d.ts +145 -0
- package/dist/tanstack-ai/index.js +120 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
SDK for obtaining scoped tokens for third-party services on behalf of apps or users. Authenticates the calling Vercel project via [`@vercel/oidc`](https://www.npmjs.com/package/@vercel/oidc) and exchanges the OIDC token for a Vercel Connect-issued credential.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Eight entrypoints, all ESM:
|
|
6
6
|
|
|
7
7
|
- `@vercel/connect` — core token / authorization SDK
|
|
8
8
|
- `@vercel/connect/chat` — adapter helpers for the [Chat SDK](https://chat-sdk.dev) (`chat`): `connectSlackAdapter`, `connectDiscordAdapter`, `connectGitHubAdapter`, `connectLinearAdapter`, `connectNotionAdapter`, `connectTelegramAdapter`, `connectSendblueAdapter`, `connectTeamsAdapter` (no Chat SDK dependency — returns structural config)
|
|
9
9
|
- `@vercel/connect/ai-sdk` — [Vercel AI SDK](https://ai-sdk.dev) glue: re-exports `connectAuthProvider` for MCP transports (optional peers: `ai`, `@ai-sdk/mcp`)
|
|
10
|
-
- `@vercel/connect/mcp` — canonical MCP-spec `OAuthClientProvider` for any MCP client (
|
|
10
|
+
- `@vercel/connect/mcp` — canonical MCP-spec `OAuthClientProvider` for any MCP client (no SDK dependency; returns a self-contained `ConnectOAuthClientProvider` assignable to `@ai-sdk/mcp` and `@modelcontextprotocol/sdk`)
|
|
11
|
+
- `@vercel/connect/tanstack-ai` — [TanStack AI](https://tanstack.com/ai) glue: `connectMCPTransport` builds a `@tanstack/ai-mcp` transport config with a Connect `authProvider` (eager consent on Streamable HTTP, spec 401 flow on SSE), plus `getConsentChallenge` (no TanStack, AI SDK, or MCP SDK dependency)
|
|
11
12
|
- `@vercel/connect/eve` — adapter helpers for [Eve](https://github.com/vercel/eve) connections (optional peer: `eve`)
|
|
12
13
|
- `@vercel/connect/betterauth` — [Better Auth](https://www.better-auth.com/) `genericOAuth` provider (optional peer: `better-auth`)
|
|
13
14
|
- `@vercel/connect/authjs` — [Auth.js](https://authjs.dev/) `OAuth2Config` provider (optional peer: `@auth/core`)
|
|
@@ -121,6 +122,62 @@ SDK's `toolApproval` option or `wrapMcpTools` from `@ai-sdk/policy-opa`.
|
|
|
121
122
|
|
|
122
123
|
Non-AI-SDK MCP clients (the official MCP TypeScript SDK, Mastra, etc.)
|
|
123
124
|
can import the same `connectAuthProvider` from `@vercel/connect/mcp`.
|
|
125
|
+
TanStack AI has its own subpath, `@vercel/connect/tanstack-ai`, described
|
|
126
|
+
below.
|
|
127
|
+
|
|
128
|
+
### TanStack AI + MCP
|
|
129
|
+
|
|
130
|
+
`@tanstack/ai-mcp`'s `createMCPClient` passes `transport.authProvider`
|
|
131
|
+
straight to the official `@modelcontextprotocol/sdk` transports.
|
|
132
|
+
`connectMCPTransport` builds that transport config with a Connect-backed
|
|
133
|
+
`authProvider` and picks the consent mode per transport: on Streamable HTTP
|
|
134
|
+
(`type: 'http'`) a missing grant fails eagerly inside `createMCPClient`, at
|
|
135
|
+
the route boundary, instead of mid-run inside a tool call where TanStack
|
|
136
|
+
would feed the error back to the model as text; on SSE it keeps the MCP-spec
|
|
137
|
+
401 flow, because the SSE EventSource would turn an eager throw into a
|
|
138
|
+
reconnect loop. TanStack wraps connect failures in `MCPConnectionError`;
|
|
139
|
+
`getConsentChallenge` looks through the `cause` chain for you. The adapter
|
|
140
|
+
also supplies a fixed discovery state so the official SDK never fetches OAuth
|
|
141
|
+
metadata from URLs the MCP server names in `WWW-Authenticate`, and the
|
|
142
|
+
transport refuses HTTP redirects by default (`redirect: 'error'`, as in
|
|
143
|
+
`@ai-sdk/mcp`).
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai';
|
|
147
|
+
import { createMCPClient } from '@tanstack/ai-mcp';
|
|
148
|
+
import {
|
|
149
|
+
connectMCPTransport,
|
|
150
|
+
getConsentChallenge,
|
|
151
|
+
} from '@vercel/connect/tanstack-ai';
|
|
152
|
+
|
|
153
|
+
export async function POST(request: Request) {
|
|
154
|
+
try {
|
|
155
|
+
const linear = await createMCPClient({
|
|
156
|
+
transport: connectMCPTransport(
|
|
157
|
+
{ type: 'http', url: 'https://mcp.linear.app/mcp' },
|
|
158
|
+
'oauth/linear',
|
|
159
|
+
{ subject: { type: 'user', id: userId } },
|
|
160
|
+
{ redirectUrl: 'https://app.example.com/integrations' }
|
|
161
|
+
),
|
|
162
|
+
});
|
|
163
|
+
const stream = chat({ adapter, messages, mcp: { clients: [linear] } });
|
|
164
|
+
return toServerSentEventsResponse(stream);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const challenge = getConsentChallenge(err);
|
|
167
|
+
if (challenge) return Response.redirect(challenge.url);
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`createMCPClients` pools take the same per-entry transport config, so one
|
|
174
|
+
`connectMCPTransport` per connector drops in. `connectAuthProvider` is also
|
|
175
|
+
re-exported for hand-built transports; only pass `consent: 'eager'` with
|
|
176
|
+
`type: 'http'`. Tool-call approval remains independent of Connect: use
|
|
177
|
+
TanStack's `needsApproval` on tool definitions. See the
|
|
178
|
+
[TanStack AI integration guide](https://github.com/vercel/vercel/blob/main/packages/connect/docs/tanstack-ai-mcp-integration.md)
|
|
179
|
+
for the eager-consent rationale, the SSE caveat, pool caveats, and the
|
|
180
|
+
`redirectUrl` fix for official-SDK transports.
|
|
124
181
|
|
|
125
182
|
### Eve
|
|
126
183
|
|
package/dist/ai-sdk/index.d.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* the AI SDK's own `toolApproval` primitive (and `wrapMcpTools` in
|
|
12
12
|
* `@ai-sdk/policy-opa`). See `docs/ai-sdk-mcp-integration.md`.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* This entrypoint has no runtime or type import of `ai` or
|
|
15
|
+
* `@ai-sdk/mcp`; `connectAuthProvider` returns the self-contained
|
|
16
|
+
* `ConnectOAuthClientProvider`, which the test suite asserts is
|
|
17
|
+
* assignable to `@ai-sdk/mcp`'s `OAuthClientProvider`. Both packages
|
|
18
|
+
* are declared as optional peers only to signal the tested version
|
|
19
|
+
* ranges.
|
|
18
20
|
*
|
|
19
21
|
* ```ts
|
|
20
22
|
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
@@ -48,5 +50,5 @@
|
|
|
48
50
|
* }
|
|
49
51
|
* ```
|
|
50
52
|
*/
|
|
51
|
-
export { connectAuthProvider, ConsentRequiredError, type ConnectAuthProviderOptions, type ConsentChallenge, } from '../mcp/connect-auth-provider.js';
|
|
53
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, type ConnectAuthProviderOptions, type ConnectConsentMode, type ConnectOAuthClientInformation, type ConnectOAuthClientMetadata, type ConnectOAuthClientProvider, type ConnectOAuthDiscoveryState, type ConnectOAuthTokens, type ConsentChallenge, } from '../mcp/connect-auth-provider.js';
|
|
52
54
|
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, type ConnectErrorOptions, type ConnectTokenExchangeSubject, type ConnectTokenParams, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from '../token.js';
|
package/dist/ai-sdk/index.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* the AI SDK's own `toolApproval` primitive (and `wrapMcpTools` in
|
|
12
12
|
* `@ai-sdk/policy-opa`). See `docs/ai-sdk-mcp-integration.md`.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* This entrypoint has no runtime or type import of `ai` or
|
|
15
|
+
* `@ai-sdk/mcp`; `connectAuthProvider` returns the self-contained
|
|
16
|
+
* `ConnectOAuthClientProvider`, which the test suite asserts is
|
|
17
|
+
* assignable to `@ai-sdk/mcp`'s `OAuthClientProvider`. Both packages
|
|
18
|
+
* are declared as optional peers only to signal the tested version
|
|
19
|
+
* ranges.
|
|
18
20
|
*
|
|
19
21
|
* ```ts
|
|
20
22
|
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
@@ -48,5 +50,5 @@
|
|
|
48
50
|
* }
|
|
49
51
|
* ```
|
|
50
52
|
*/
|
|
51
|
-
export { connectAuthProvider, ConsentRequiredError, } from '../mcp/connect-auth-provider.js';
|
|
53
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, } from '../mcp/connect-auth-provider.js';
|
|
52
54
|
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { deleteTokenCacheEntry, getToken, getTokenResponse, revokeToken, Connect
|
|
|
2
2
|
export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from './authorization.js';
|
|
3
3
|
export { experimental_startInstallation, type ConnectInstallationOptions, type ConnectInstallationParams, type ConnectInstallationResponse, } from './installation.js';
|
|
4
4
|
export type { ConnectAuthorizationDetail } from './authorization-details.js';
|
|
5
|
-
export { getConnectorMetadata, type ConnectorMetadata
|
|
5
|
+
export { getConnectorMetadata, type ConnectorMetadata } from './connector.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { deleteTokenCacheEntry, getToken, getTokenResponse, revokeToken, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from './token.js';
|
|
2
2
|
export { startAuthorization, } from './authorization.js';
|
|
3
3
|
export { experimental_startInstallation, } from './installation.js';
|
|
4
|
-
export { getConnectorMetadata
|
|
4
|
+
export { getConnectorMetadata } from './connector.js';
|
|
@@ -1,5 +1,117 @@
|
|
|
1
|
-
import type { OAuthClientProvider } from '@ai-sdk/mcp';
|
|
2
1
|
import { type ConnectTokenParams } from '../token.js';
|
|
2
|
+
/**
|
|
3
|
+
* OAuth token set as consumed by MCP client transports (RFC 6749 §5.1).
|
|
4
|
+
* Structurally identical to `OAuthTokens` in both `@ai-sdk/mcp` and
|
|
5
|
+
* `@modelcontextprotocol/sdk`; declared here so this module has no
|
|
6
|
+
* type dependency on either package.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConnectOAuthTokens {
|
|
9
|
+
access_token: string;
|
|
10
|
+
token_type: string;
|
|
11
|
+
expires_in?: number;
|
|
12
|
+
scope?: string;
|
|
13
|
+
refresh_token?: string;
|
|
14
|
+
id_token?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Minimal OAuth client metadata (RFC 7591 §2). Only consulted by MCP
|
|
18
|
+
* transports during dynamic client registration, which the Connect
|
|
19
|
+
* adapter never triggers.
|
|
20
|
+
*/
|
|
21
|
+
export interface ConnectOAuthClientMetadata {
|
|
22
|
+
redirect_uris: string[];
|
|
23
|
+
}
|
|
24
|
+
/** OAuth client identity reported to MCP transports. */
|
|
25
|
+
export interface ConnectOAuthClientInformation {
|
|
26
|
+
client_id: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Structural mirror of the official SDK's `OAuthDiscoveryState`: the
|
|
30
|
+
* result of RFC 9728 protected-resource discovery plus authorization
|
|
31
|
+
* server metadata. {@link connectAuthProvider} returns a fixed state
|
|
32
|
+
* naming Connect as the authorization server so the SDK never performs
|
|
33
|
+
* network discovery; see {@link CONNECT_DISCOVERY_STATE}.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConnectOAuthDiscoveryState {
|
|
36
|
+
authorizationServerUrl: string;
|
|
37
|
+
authorizationServerMetadata: {
|
|
38
|
+
issuer: string;
|
|
39
|
+
authorization_endpoint: string;
|
|
40
|
+
token_endpoint: string;
|
|
41
|
+
response_types_supported: string[];
|
|
42
|
+
code_challenge_methods_supported: string[];
|
|
43
|
+
};
|
|
44
|
+
resourceMetadata: {
|
|
45
|
+
resource: string;
|
|
46
|
+
authorization_servers: string[];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The MCP-spec `OAuthClientProvider` members implemented by
|
|
51
|
+
* {@link connectAuthProvider}.
|
|
52
|
+
*
|
|
53
|
+
* This is the intersection of the required surface of
|
|
54
|
+
* `OAuthClientProvider` from `@ai-sdk/mcp` and from the official
|
|
55
|
+
* `@modelcontextprotocol/sdk` (`client/auth.js`), so a value of this
|
|
56
|
+
* type is assignable to either interface without a cast. The optional
|
|
57
|
+
* members those interfaces declare (`state`, `invalidateCredentials`,
|
|
58
|
+
* `addClientAuthentication`, `prepareTokenRequest`, discovery-state
|
|
59
|
+
* hooks, …) are deliberately omitted: Connect owns registration, PKCE,
|
|
60
|
+
* refresh, and storage server-side, and declaring them here would tie
|
|
61
|
+
* this type to one SDK's parameter types. The test suite asserts
|
|
62
|
+
* assignability against both SDKs.
|
|
63
|
+
*
|
|
64
|
+
* The exceptions are the three discovery hooks at the bottom. They
|
|
65
|
+
* are optional in the official SDK's interface and absent from
|
|
66
|
+
* `@ai-sdk/mcp`'s, and are implemented here so the official SDK's
|
|
67
|
+
* `auth()` never fetches discovery documents from URLs chosen by the
|
|
68
|
+
* MCP server's `WWW-Authenticate` header (a blind-SSRF vector when
|
|
69
|
+
* the configured server is compromised). Connect is the authorization
|
|
70
|
+
* server from this adapter's point of view, so there is nothing to
|
|
71
|
+
* discover.
|
|
72
|
+
*/
|
|
73
|
+
export interface ConnectOAuthClientProvider {
|
|
74
|
+
tokens(): Promise<ConnectOAuthTokens | undefined>;
|
|
75
|
+
saveTokens(tokens: ConnectOAuthTokens): void;
|
|
76
|
+
redirectToAuthorization(authorizationUrl: URL): Promise<void>;
|
|
77
|
+
saveCodeVerifier(codeVerifier: string): void;
|
|
78
|
+
codeVerifier(): string;
|
|
79
|
+
get redirectUrl(): string;
|
|
80
|
+
get clientMetadata(): ConnectOAuthClientMetadata;
|
|
81
|
+
clientInformation(): ConnectOAuthClientInformation;
|
|
82
|
+
discoveryState(): ConnectOAuthDiscoveryState;
|
|
83
|
+
saveDiscoveryState(state: unknown): void;
|
|
84
|
+
validateResourceURL(serverUrl: string | URL, resource?: string): Promise<URL | undefined>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* How {@link connectAuthProvider} surfaces a missing Connect grant.
|
|
88
|
+
*
|
|
89
|
+
* - `'transport'` (default) — `tokens()` returns `undefined` and the
|
|
90
|
+
* MCP transport's own 401 → `auth()` orchestrator eventually calls
|
|
91
|
+
* `redirectToAuthorization`, where the Connect consent URL is
|
|
92
|
+
* raised. This is the MCP-spec contract and works with every
|
|
93
|
+
* transport in `@ai-sdk/mcp` and `@modelcontextprotocol/sdk`.
|
|
94
|
+
* - `'eager'` — `tokens()` itself raises the consent challenge, before
|
|
95
|
+
* any request reaches the MCP server. Connect authoritatively knows
|
|
96
|
+
* whether a grant exists, so this skips the transport's
|
|
97
|
+
* protected-resource discovery round-trips and surfaces the
|
|
98
|
+
* challenge on the very first request (the MCP `initialize`
|
|
99
|
+
* handshake) instead of mid-run inside a tool call.
|
|
100
|
+
*
|
|
101
|
+
* **Streamable HTTP only.** The official SDK's
|
|
102
|
+
* `StreamableHTTPClientTransport` (and `@ai-sdk/mcp`'s HTTP
|
|
103
|
+
* transport) call `tokens()` per request and let a throw propagate
|
|
104
|
+
* to the caller. The official `SSEClientTransport` instead calls
|
|
105
|
+
* `tokens()` inside the `fetch` it hands to its EventSource, which
|
|
106
|
+
* treats any throw as a retryable network error: the consent error
|
|
107
|
+
* is flattened to a cause-less `SseError` and the EventSource
|
|
108
|
+
* reconnects every few seconds, minting a new Connect authorization
|
|
109
|
+
* request each time, until the transport is closed. Never combine
|
|
110
|
+
* `'eager'` with an SSE transport; `connectMCPTransport` in
|
|
111
|
+
* `@vercel/connect/tanstack-ai` selects the mode per transport type
|
|
112
|
+
* for you.
|
|
113
|
+
*/
|
|
114
|
+
export type ConnectConsentMode = 'transport' | 'eager';
|
|
3
115
|
/** Options accepted by {@link connectAuthProvider}. */
|
|
4
116
|
export interface ConnectAuthProviderOptions {
|
|
5
117
|
/**
|
|
@@ -20,8 +132,16 @@ export interface ConnectAuthProviderOptions {
|
|
|
20
132
|
* `startAuthorization` (as its `callbackUrl`) and also surfaced
|
|
21
133
|
* through the MCP-spec `OAuthClientProvider.redirectUrl` getter.
|
|
22
134
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
135
|
+
* When omitted, Connect falls back to the connector's server-side
|
|
136
|
+
* registered redirect, and the `redirectUrl` getter reports
|
|
137
|
+
* {@link CONNECT_MANAGED_REDIRECT_URL} instead of an empty string.
|
|
138
|
+
* The official `@modelcontextprotocol/sdk` treats a falsy
|
|
139
|
+
* `redirectUrl` as a request for its non-interactive
|
|
140
|
+
* client-credentials flow, which would bypass Connect's consent
|
|
141
|
+
* page entirely; a non-empty placeholder keeps that transport on
|
|
142
|
+
* the interactive path. The placeholder is never used as a real
|
|
143
|
+
* redirect: `redirectToAuthorization` discards the SDK-built
|
|
144
|
+
* authorization URL in favour of Connect's own consent URL.
|
|
25
145
|
*/
|
|
26
146
|
readonly redirectUrl?: string;
|
|
27
147
|
/**
|
|
@@ -35,10 +155,29 @@ export interface ConnectAuthProviderOptions {
|
|
|
35
155
|
* Called when Vercel Connect reports that the user has not yet
|
|
36
156
|
* authorized the connector. The caller decides how to surface the
|
|
37
157
|
* consent URL — `redirect()`, throw a typed error, emit a custom
|
|
38
|
-
* UI chunk, etc.
|
|
39
|
-
*
|
|
158
|
+
* UI chunk, etc.
|
|
159
|
+
*
|
|
160
|
+
* In `'transport'` consent mode the callback replaces the default
|
|
161
|
+
* {@link ConsentRequiredError}: if it returns normally, this adapter
|
|
162
|
+
* throws nothing. The MCP transport still fails the request with
|
|
163
|
+
* its own generic `UnauthorizedError` (which carries no `cause`, so
|
|
164
|
+
* {@link getConsentChallenge} cannot recover the URL from it); a
|
|
165
|
+
* non-throwing callback is therefore only useful for side effects
|
|
166
|
+
* such as logging or emitting a UI chunk, or when the callback
|
|
167
|
+
* itself redirects by throwing (e.g. a framework `redirect()`).
|
|
168
|
+
*
|
|
169
|
+
* In `'eager'` mode the callback runs first and
|
|
170
|
+
* {@link ConsentRequiredError} is thrown regardless, because
|
|
171
|
+
* `tokens()` must abort the outgoing request; returning `undefined`
|
|
172
|
+
* there would let the transport proceed unauthenticated and raise
|
|
173
|
+
* the challenge a second time.
|
|
40
174
|
*/
|
|
41
175
|
readonly onConsentRequired?: (challenge: ConsentChallenge) => void | Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Where the consent challenge is raised. See
|
|
178
|
+
* {@link ConnectConsentMode}. Defaults to `'transport'`.
|
|
179
|
+
*/
|
|
180
|
+
readonly consent?: ConnectConsentMode;
|
|
42
181
|
}
|
|
43
182
|
/**
|
|
44
183
|
* Returned to {@link ConnectAuthProviderOptions.onConsentRequired} (or
|
|
@@ -68,6 +207,11 @@ export interface ConsentChallenge {
|
|
|
68
207
|
* the configured subject. Catch at the boundary
|
|
69
208
|
* (`route handler` / `streamText` call site) and redirect to
|
|
70
209
|
* `error.url`.
|
|
210
|
+
*
|
|
211
|
+
* MCP client libraries often wrap connection failures in their own
|
|
212
|
+
* error type (for example `@tanstack/ai-mcp`'s `MCPConnectionError`)
|
|
213
|
+
* with this error on `cause`. Use {@link getConsentChallenge} to find
|
|
214
|
+
* it anywhere in a `cause` chain.
|
|
71
215
|
*/
|
|
72
216
|
export declare class ConsentRequiredError extends Error {
|
|
73
217
|
readonly name = "ConsentRequiredError";
|
|
@@ -81,13 +225,79 @@ export declare class ConsentRequiredError extends Error {
|
|
|
81
225
|
constructor(challenge: ConsentChallenge);
|
|
82
226
|
}
|
|
83
227
|
/**
|
|
84
|
-
*
|
|
228
|
+
* Type guard for {@link ConsentRequiredError}. Only matches the error
|
|
229
|
+
* itself; use {@link getConsentChallenge} to look through wrapping
|
|
230
|
+
* errors' `cause` chains.
|
|
231
|
+
*/
|
|
232
|
+
export declare function isConsentRequiredError(err: unknown): err is ConsentRequiredError;
|
|
233
|
+
/**
|
|
234
|
+
* Finds a Connect consent challenge in `err` or anywhere along its
|
|
235
|
+
* `cause` chain and returns it, or `undefined` when none is present.
|
|
236
|
+
*
|
|
237
|
+
* MCP clients typically raise the adapter's {@link ConsentRequiredError}
|
|
238
|
+
* from inside their connect / tool-call machinery and re-wrap it —
|
|
239
|
+
* `@tanstack/ai-mcp`'s `createMCPClient` throws `MCPConnectionError`
|
|
240
|
+
* with the original error on `cause`. This helper lets a route handler
|
|
241
|
+
* branch on consent without knowing the wrapping shape:
|
|
242
|
+
*
|
|
243
|
+
* ```ts
|
|
244
|
+
* } catch (err) {
|
|
245
|
+
* const challenge = getConsentChallenge(err);
|
|
246
|
+
* if (challenge) return Response.redirect(challenge.url);
|
|
247
|
+
* throw err;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* Matching is by `instanceof` first, then structurally on
|
|
252
|
+
* `name === 'ConsentRequiredError'` so a duplicated copy of
|
|
253
|
+
* `@vercel/connect` in the dependency graph is still recognized.
|
|
254
|
+
*/
|
|
255
|
+
export declare function getConsentChallenge(err: unknown): ConsentChallenge | undefined;
|
|
256
|
+
/**
|
|
257
|
+
* Value reported by `OAuthClientProvider.redirectUrl` when no
|
|
258
|
+
* {@link ConnectAuthProviderOptions.redirectUrl} is configured.
|
|
259
|
+
*
|
|
260
|
+
* Connect's hosted consent flow owns the real post-consent redirect
|
|
261
|
+
* (the connector's server-side registered redirect), so this URL is
|
|
262
|
+
* never followed. It exists only so the official
|
|
263
|
+
* `@modelcontextprotocol/sdk` `auth()` orchestrator — which treats a
|
|
264
|
+
* falsy `redirectUrl` as "run the non-interactive client-credentials
|
|
265
|
+
* flow" — stays on the interactive path and reaches
|
|
266
|
+
* `redirectToAuthorization`.
|
|
267
|
+
*/
|
|
268
|
+
export declare const CONNECT_MANAGED_REDIRECT_URL = "https://connect.vercel.com/";
|
|
269
|
+
/**
|
|
270
|
+
* Discovery state returned by `OAuthClientProvider.discoveryState()`.
|
|
271
|
+
*
|
|
272
|
+
* The official `@modelcontextprotocol/sdk` `auth()` orchestrator runs
|
|
273
|
+
* RFC 9728 discovery on every 401: it fetches the URL named by the MCP
|
|
274
|
+
* server's `WWW-Authenticate: Bearer resource_metadata="…"` parameter,
|
|
275
|
+
* then the authorization-server metadata at whatever host that
|
|
276
|
+
* document names. Neither URL is validated, so a compromised MCP
|
|
277
|
+
* server can point them at private-network addresses (blind SSRF).
|
|
278
|
+
* When a provider returns a complete cached state the orchestrator
|
|
279
|
+
* skips every discovery fetch, so this adapter returns one that names
|
|
280
|
+
* Connect as the authorization server. None of these endpoints is
|
|
281
|
+
* ever called: `redirectToAuthorization` discards the authorization
|
|
282
|
+
* URL the SDK builds from them in favour of Connect's own consent URL,
|
|
283
|
+
* and the token endpoint is only used for refresh and code exchange,
|
|
284
|
+
* both of which Connect performs server-side.
|
|
285
|
+
*/
|
|
286
|
+
export declare const CONNECT_DISCOVERY_STATE: ConnectOAuthDiscoveryState;
|
|
287
|
+
/**
|
|
288
|
+
* Builds an MCP-spec `OAuthClientProvider` backed by Vercel
|
|
85
289
|
* Connect. The returned object delegates `tokens()` to
|
|
86
290
|
* {@link getTokenResponse} and `redirectToAuthorization` to
|
|
87
291
|
* {@link startAuthorization}. Connect owns client registration, PKCE,
|
|
88
292
|
* state, and the callback handshake server-side, so most of the
|
|
89
293
|
* `OAuthClientProvider` surface is intentionally a no-op.
|
|
90
294
|
*
|
|
295
|
+
* The returned object satisfies both `@ai-sdk/mcp`'s and the official
|
|
296
|
+
* `@modelcontextprotocol/sdk`'s `OAuthClientProvider` interfaces, so
|
|
297
|
+
* it plugs into `createMCPClient` from either `@ai-sdk/mcp` or
|
|
298
|
+
* `@tanstack/ai-mcp`, and into a hand-built
|
|
299
|
+
* `StreamableHTTPClientTransport`.
|
|
300
|
+
*
|
|
91
301
|
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
92
302
|
* or opaque connector id.
|
|
93
303
|
* @param params Token request parameters — always specify a
|
|
@@ -96,4 +306,4 @@ export declare class ConsentRequiredError extends Error {
|
|
|
96
306
|
* security semantics.
|
|
97
307
|
* @param options Optional adapter behavior overrides.
|
|
98
308
|
*/
|
|
99
|
-
export declare function connectAuthProvider(connector: string, params: ConnectTokenParams, options?: ConnectAuthProviderOptions):
|
|
309
|
+
export declare function connectAuthProvider(connector: string, params: ConnectTokenParams, options?: ConnectAuthProviderOptions): ConnectOAuthClientProvider;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { startAuthorization } from '../authorization.js';
|
|
2
|
-
import {
|
|
2
|
+
import { UserAuthorizationRequiredError, getTokenResponse, } from '../token.js';
|
|
3
3
|
/**
|
|
4
4
|
* Thrown by the {@link connectAuthProvider} default
|
|
5
5
|
* `onConsentRequired` handler when the user has no Connect grant for
|
|
6
6
|
* the configured subject. Catch at the boundary
|
|
7
7
|
* (`route handler` / `streamText` call site) and redirect to
|
|
8
8
|
* `error.url`.
|
|
9
|
+
*
|
|
10
|
+
* MCP client libraries often wrap connection failures in their own
|
|
11
|
+
* error type (for example `@tanstack/ai-mcp`'s `MCPConnectionError`)
|
|
12
|
+
* with this error on `cause`. Use {@link getConsentChallenge} to find
|
|
13
|
+
* it anywhere in a `cause` chain.
|
|
9
14
|
*/
|
|
10
15
|
export class ConsentRequiredError extends Error {
|
|
11
16
|
name = 'ConsentRequiredError';
|
|
@@ -27,17 +32,144 @@ export class ConsentRequiredError extends Error {
|
|
|
27
32
|
this.expiresAt = challenge.expiresAt;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Type guard for {@link ConsentRequiredError}. Only matches the error
|
|
37
|
+
* itself; use {@link getConsentChallenge} to look through wrapping
|
|
38
|
+
* errors' `cause` chains.
|
|
39
|
+
*/
|
|
40
|
+
export function isConsentRequiredError(err) {
|
|
41
|
+
return err instanceof ConsentRequiredError;
|
|
42
|
+
}
|
|
43
|
+
/** Upper bound on `cause` hops walked by {@link getConsentChallenge}. */
|
|
44
|
+
const MAX_CAUSE_DEPTH = 16;
|
|
45
|
+
/**
|
|
46
|
+
* Finds a Connect consent challenge in `err` or anywhere along its
|
|
47
|
+
* `cause` chain and returns it, or `undefined` when none is present.
|
|
48
|
+
*
|
|
49
|
+
* MCP clients typically raise the adapter's {@link ConsentRequiredError}
|
|
50
|
+
* from inside their connect / tool-call machinery and re-wrap it —
|
|
51
|
+
* `@tanstack/ai-mcp`'s `createMCPClient` throws `MCPConnectionError`
|
|
52
|
+
* with the original error on `cause`. This helper lets a route handler
|
|
53
|
+
* branch on consent without knowing the wrapping shape:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* } catch (err) {
|
|
57
|
+
* const challenge = getConsentChallenge(err);
|
|
58
|
+
* if (challenge) return Response.redirect(challenge.url);
|
|
59
|
+
* throw err;
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Matching is by `instanceof` first, then structurally on
|
|
64
|
+
* `name === 'ConsentRequiredError'` so a duplicated copy of
|
|
65
|
+
* `@vercel/connect` in the dependency graph is still recognized.
|
|
66
|
+
*/
|
|
67
|
+
export function getConsentChallenge(err) {
|
|
68
|
+
let current = err;
|
|
69
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
|
|
70
|
+
if (current === null || typeof current !== 'object') {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const challenge = toConsentChallenge(current);
|
|
74
|
+
if (challenge) {
|
|
75
|
+
return challenge;
|
|
76
|
+
}
|
|
77
|
+
current = current.cause;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
function toConsentChallenge(candidate) {
|
|
82
|
+
if (candidate instanceof ConsentRequiredError) {
|
|
83
|
+
return {
|
|
84
|
+
connector: candidate.connector,
|
|
85
|
+
subject: candidate.subject,
|
|
86
|
+
url: candidate.url,
|
|
87
|
+
request: candidate.request,
|
|
88
|
+
verifier: candidate.verifier,
|
|
89
|
+
deviceCode: candidate.deviceCode,
|
|
90
|
+
expiresAt: candidate.expiresAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const value = candidate;
|
|
94
|
+
if (value.name === 'ConsentRequiredError' &&
|
|
95
|
+
typeof value.url === 'string' &&
|
|
96
|
+
typeof value.connector === 'string' &&
|
|
97
|
+
typeof value.request === 'string' &&
|
|
98
|
+
typeof value.verifier === 'string' &&
|
|
99
|
+
value.subject !== undefined) {
|
|
100
|
+
return {
|
|
101
|
+
connector: value.connector,
|
|
102
|
+
subject: value.subject,
|
|
103
|
+
url: value.url,
|
|
104
|
+
request: value.request,
|
|
105
|
+
verifier: value.verifier,
|
|
106
|
+
deviceCode: value.deviceCode,
|
|
107
|
+
expiresAt: value.expiresAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Value reported by `OAuthClientProvider.redirectUrl` when no
|
|
114
|
+
* {@link ConnectAuthProviderOptions.redirectUrl} is configured.
|
|
115
|
+
*
|
|
116
|
+
* Connect's hosted consent flow owns the real post-consent redirect
|
|
117
|
+
* (the connector's server-side registered redirect), so this URL is
|
|
118
|
+
* never followed. It exists only so the official
|
|
119
|
+
* `@modelcontextprotocol/sdk` `auth()` orchestrator — which treats a
|
|
120
|
+
* falsy `redirectUrl` as "run the non-interactive client-credentials
|
|
121
|
+
* flow" — stays on the interactive path and reaches
|
|
122
|
+
* `redirectToAuthorization`.
|
|
123
|
+
*/
|
|
124
|
+
export const CONNECT_MANAGED_REDIRECT_URL = 'https://connect.vercel.com/';
|
|
125
|
+
/**
|
|
126
|
+
* Discovery state returned by `OAuthClientProvider.discoveryState()`.
|
|
127
|
+
*
|
|
128
|
+
* The official `@modelcontextprotocol/sdk` `auth()` orchestrator runs
|
|
129
|
+
* RFC 9728 discovery on every 401: it fetches the URL named by the MCP
|
|
130
|
+
* server's `WWW-Authenticate: Bearer resource_metadata="…"` parameter,
|
|
131
|
+
* then the authorization-server metadata at whatever host that
|
|
132
|
+
* document names. Neither URL is validated, so a compromised MCP
|
|
133
|
+
* server can point them at private-network addresses (blind SSRF).
|
|
134
|
+
* When a provider returns a complete cached state the orchestrator
|
|
135
|
+
* skips every discovery fetch, so this adapter returns one that names
|
|
136
|
+
* Connect as the authorization server. None of these endpoints is
|
|
137
|
+
* ever called: `redirectToAuthorization` discards the authorization
|
|
138
|
+
* URL the SDK builds from them in favour of Connect's own consent URL,
|
|
139
|
+
* and the token endpoint is only used for refresh and code exchange,
|
|
140
|
+
* both of which Connect performs server-side.
|
|
141
|
+
*/
|
|
142
|
+
export const CONNECT_DISCOVERY_STATE = {
|
|
143
|
+
authorizationServerUrl: 'https://connect.vercel.com',
|
|
144
|
+
authorizationServerMetadata: {
|
|
145
|
+
issuer: 'https://connect.vercel.com',
|
|
146
|
+
authorization_endpoint: 'https://connect.vercel.com/',
|
|
147
|
+
token_endpoint: 'https://connect.vercel.com/',
|
|
148
|
+
response_types_supported: ['code'],
|
|
149
|
+
code_challenge_methods_supported: ['S256'],
|
|
150
|
+
},
|
|
151
|
+
resourceMetadata: {
|
|
152
|
+
resource: 'https://connect.vercel.com/',
|
|
153
|
+
authorization_servers: ['https://connect.vercel.com'],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
30
156
|
const EMPTY_CLIENT_METADATA = {
|
|
31
157
|
redirect_uris: [],
|
|
32
158
|
};
|
|
33
159
|
/**
|
|
34
|
-
* Builds an MCP-spec
|
|
160
|
+
* Builds an MCP-spec `OAuthClientProvider` backed by Vercel
|
|
35
161
|
* Connect. The returned object delegates `tokens()` to
|
|
36
162
|
* {@link getTokenResponse} and `redirectToAuthorization` to
|
|
37
163
|
* {@link startAuthorization}. Connect owns client registration, PKCE,
|
|
38
164
|
* state, and the callback handshake server-side, so most of the
|
|
39
165
|
* `OAuthClientProvider` surface is intentionally a no-op.
|
|
40
166
|
*
|
|
167
|
+
* The returned object satisfies both `@ai-sdk/mcp`'s and the official
|
|
168
|
+
* `@modelcontextprotocol/sdk`'s `OAuthClientProvider` interfaces, so
|
|
169
|
+
* it plugs into `createMCPClient` from either `@ai-sdk/mcp` or
|
|
170
|
+
* `@tanstack/ai-mcp`, and into a hand-built
|
|
171
|
+
* `StreamableHTTPClientTransport`.
|
|
172
|
+
*
|
|
41
173
|
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
42
174
|
* or opaque connector id.
|
|
43
175
|
* @param params Token request parameters — always specify a
|
|
@@ -47,7 +179,8 @@ const EMPTY_CLIENT_METADATA = {
|
|
|
47
179
|
* @param options Optional adapter behavior overrides.
|
|
48
180
|
*/
|
|
49
181
|
export function connectAuthProvider(connector, params, options) {
|
|
50
|
-
const
|
|
182
|
+
const configuredRedirectUrl = options?.redirectUrl;
|
|
183
|
+
const consentMode = options?.consent ?? 'transport';
|
|
51
184
|
async function resolveVercelToken() {
|
|
52
185
|
const { vercelToken } = options ?? {};
|
|
53
186
|
if (typeof vercelToken === 'function') {
|
|
@@ -55,6 +188,27 @@ export function connectAuthProvider(connector, params, options) {
|
|
|
55
188
|
}
|
|
56
189
|
return vercelToken;
|
|
57
190
|
}
|
|
191
|
+
/** Asks Connect for a consent URL for the configured subject. */
|
|
192
|
+
async function mintConsentChallenge() {
|
|
193
|
+
const vercelToken = await resolveVercelToken();
|
|
194
|
+
const response = await startAuthorization(connector, params, {
|
|
195
|
+
...(vercelToken !== undefined && { vercelToken }),
|
|
196
|
+
// Forward the post-consent return URL so the user lands back
|
|
197
|
+
// in the app. Omitted => Connect uses the connector's
|
|
198
|
+
// registered redirect.
|
|
199
|
+
...(configuredRedirectUrl && { callbackUrl: configuredRedirectUrl }),
|
|
200
|
+
...(options?.deviceCode && { deviceCode: true }),
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
connector,
|
|
204
|
+
subject: params.subject,
|
|
205
|
+
url: response.url,
|
|
206
|
+
request: response.request,
|
|
207
|
+
verifier: response.verifier,
|
|
208
|
+
deviceCode: response.deviceCode,
|
|
209
|
+
expiresAt: response.expiresAt,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
58
212
|
return {
|
|
59
213
|
async tokens() {
|
|
60
214
|
try {
|
|
@@ -68,42 +222,34 @@ export function connectAuthProvider(connector, params, options) {
|
|
|
68
222
|
};
|
|
69
223
|
}
|
|
70
224
|
catch (err) {
|
|
71
|
-
if (err instanceof UserAuthorizationRequiredError) {
|
|
225
|
+
if (!(err instanceof UserAuthorizationRequiredError)) {
|
|
226
|
+
// ConnectorInstallationRequiredError (a configuration
|
|
227
|
+
// problem, not a per-user consent issue) and everything
|
|
228
|
+
// else propagate as-is rather than masquerading as a
|
|
229
|
+
// consent challenge.
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
if (consentMode === 'transport') {
|
|
72
233
|
// Returning undefined lets the MCP transport's auth()
|
|
73
234
|
// orchestrator trigger redirectToAuthorization below,
|
|
74
235
|
// where we surface the Connect consent URL.
|
|
75
236
|
return undefined;
|
|
76
237
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
238
|
+
// Eager: surface Connect's consent URL right here, before
|
|
239
|
+
// the transport sends anything. Connect is authoritative
|
|
240
|
+
// about the missing grant, so there is nothing to gain from
|
|
241
|
+
// letting the server 401 first. The callback runs for side
|
|
242
|
+
// effects, then the request is aborted unconditionally.
|
|
243
|
+
const challenge = await mintConsentChallenge();
|
|
244
|
+
await options?.onConsentRequired?.(challenge);
|
|
245
|
+
throw new ConsentRequiredError(challenge);
|
|
83
246
|
}
|
|
84
247
|
},
|
|
85
248
|
async redirectToAuthorization(_authorizationUrl) {
|
|
86
249
|
// The URL argument is the MCP server's discovered authorization
|
|
87
250
|
// endpoint. We ignore it — Connect has its own consent URL
|
|
88
251
|
// backed by the connector's registered OAuth client.
|
|
89
|
-
const
|
|
90
|
-
const response = await startAuthorization(connector, params, {
|
|
91
|
-
...(vercelToken !== undefined && { vercelToken }),
|
|
92
|
-
// Forward the post-consent return URL so the user lands back
|
|
93
|
-
// in the app. Falsy (the default empty string) => Connect uses
|
|
94
|
-
// the connector's registered redirect.
|
|
95
|
-
...(redirectUrl && { callbackUrl: redirectUrl }),
|
|
96
|
-
...(options?.deviceCode && { deviceCode: true }),
|
|
97
|
-
});
|
|
98
|
-
const challenge = {
|
|
99
|
-
connector,
|
|
100
|
-
subject: params.subject,
|
|
101
|
-
url: response.url,
|
|
102
|
-
request: response.request,
|
|
103
|
-
verifier: response.verifier,
|
|
104
|
-
deviceCode: response.deviceCode,
|
|
105
|
-
expiresAt: response.expiresAt,
|
|
106
|
-
};
|
|
252
|
+
const challenge = await mintConsentChallenge();
|
|
107
253
|
if (options?.onConsentRequired) {
|
|
108
254
|
await options.onConsentRequired(challenge);
|
|
109
255
|
return;
|
|
@@ -126,10 +272,13 @@ export function connectAuthProvider(connector, params, options) {
|
|
|
126
272
|
throw new Error('@vercel/connect: OAuthClientProvider.codeVerifier() is unsupported. Vercel Connect owns the PKCE flow server-side; this method should never be invoked by a correctly-configured MCP transport.');
|
|
127
273
|
},
|
|
128
274
|
get redirectUrl() {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
275
|
+
// The configured post-consent return URL, or a non-empty
|
|
276
|
+
// placeholder. Must never be falsy: the official MCP SDK's
|
|
277
|
+
// auth() reads `!provider.redirectUrl` as "non-interactive
|
|
278
|
+
// flow" and would try a client-credentials token request
|
|
279
|
+
// against the third-party authorization server instead of
|
|
280
|
+
// calling redirectToAuthorization.
|
|
281
|
+
return configuredRedirectUrl || CONNECT_MANAGED_REDIRECT_URL;
|
|
133
282
|
},
|
|
134
283
|
get clientMetadata() {
|
|
135
284
|
// Returned only during dynamic client registration, which we
|
|
@@ -144,5 +293,21 @@ export function connectAuthProvider(connector, params, options) {
|
|
|
144
293
|
// returning a synthetic value.
|
|
145
294
|
return { client_id: connector };
|
|
146
295
|
},
|
|
296
|
+
discoveryState() {
|
|
297
|
+
// Pre-empts RFC 9728 discovery so the official SDK never fetches
|
|
298
|
+
// URLs chosen by the MCP server's WWW-Authenticate header.
|
|
299
|
+
return CONNECT_DISCOVERY_STATE;
|
|
300
|
+
},
|
|
301
|
+
saveDiscoveryState(_state) {
|
|
302
|
+
// No-op: the state above is fixed and never needs persisting.
|
|
303
|
+
},
|
|
304
|
+
async validateResourceURL() {
|
|
305
|
+
// Bypasses the SDK's check that the synthetic resource metadata
|
|
306
|
+
// matches the MCP server URL (it would throw before reaching
|
|
307
|
+
// redirectToAuthorization). Returning undefined also omits the
|
|
308
|
+
// RFC 8707 `resource` parameter from the authorization URL the
|
|
309
|
+
// SDK builds, which is discarded anyway.
|
|
310
|
+
return undefined;
|
|
311
|
+
},
|
|
147
312
|
};
|
|
148
313
|
}
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* `@vercel/connect/ai-sdk` instead; that subpath re-exports from
|
|
10
10
|
* here.
|
|
11
11
|
*
|
|
12
|
-
* `@ai-sdk/mcp`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* This entrypoint has no runtime or type import of `@ai-sdk/mcp` or
|
|
13
|
+
* `@modelcontextprotocol/sdk`; `connectAuthProvider` returns the
|
|
14
|
+
* self-contained `ConnectOAuthClientProvider`, which the test suite
|
|
15
|
+
* asserts is assignable to both SDKs' `OAuthClientProvider`. Both
|
|
16
|
+
* packages are declared as optional peers only to signal the tested
|
|
17
|
+
* version ranges.
|
|
15
18
|
*
|
|
16
19
|
* ```ts
|
|
17
20
|
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
@@ -29,5 +32,5 @@
|
|
|
29
32
|
* });
|
|
30
33
|
* ```
|
|
31
34
|
*/
|
|
32
|
-
export { connectAuthProvider, ConsentRequiredError, type ConnectAuthProviderOptions, type ConsentChallenge, } from './connect-auth-provider.js';
|
|
35
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, type ConnectAuthProviderOptions, type ConnectConsentMode, type ConnectOAuthClientInformation, type ConnectOAuthClientMetadata, type ConnectOAuthClientProvider, type ConnectOAuthDiscoveryState, type ConnectOAuthTokens, type ConsentChallenge, } from './connect-auth-provider.js';
|
|
33
36
|
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, type ConnectErrorOptions, type ConnectTokenExchangeSubject, type ConnectTokenParams, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from '../token.js';
|
package/dist/mcp/index.js
CHANGED
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* `@vercel/connect/ai-sdk` instead; that subpath re-exports from
|
|
10
10
|
* here.
|
|
11
11
|
*
|
|
12
|
-
* `@ai-sdk/mcp`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* This entrypoint has no runtime or type import of `@ai-sdk/mcp` or
|
|
13
|
+
* `@modelcontextprotocol/sdk`; `connectAuthProvider` returns the
|
|
14
|
+
* self-contained `ConnectOAuthClientProvider`, which the test suite
|
|
15
|
+
* asserts is assignable to both SDKs' `OAuthClientProvider`. Both
|
|
16
|
+
* packages are declared as optional peers only to signal the tested
|
|
17
|
+
* version ranges.
|
|
15
18
|
*
|
|
16
19
|
* ```ts
|
|
17
20
|
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
@@ -29,5 +32,5 @@
|
|
|
29
32
|
* });
|
|
30
33
|
* ```
|
|
31
34
|
*/
|
|
32
|
-
export { connectAuthProvider, ConsentRequiredError, } from './connect-auth-provider.js';
|
|
35
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, } from './connect-auth-provider.js';
|
|
33
36
|
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/tanstack-ai` subpath.
|
|
3
|
+
*
|
|
4
|
+
* [TanStack AI](https://tanstack.com/ai)'s MCP client
|
|
5
|
+
* (`@tanstack/ai-mcp`) is built directly on the official
|
|
6
|
+
* `@modelcontextprotocol/sdk` transports and accepts any MCP-spec
|
|
7
|
+
* `OAuthClientProvider` on its `http` / `sse` transport config. This
|
|
8
|
+
* subpath adds one TanStack-shaped helper on top of the shared Connect
|
|
9
|
+
* adapter:
|
|
10
|
+
*
|
|
11
|
+
* - {@link connectMCPTransport} builds the whole
|
|
12
|
+
* `createMCPClient({ transport })` config and picks the consent mode
|
|
13
|
+
* per transport type: `'eager'` for Streamable HTTP, `'transport'`
|
|
14
|
+
* for SSE. Eager consent makes `tokens()` raise the Connect consent
|
|
15
|
+
* challenge before the first request (the MCP `initialize`
|
|
16
|
+
* handshake) leaves the process, so a missing grant always fails at
|
|
17
|
+
* the route boundary, never mid-run inside a tool call where
|
|
18
|
+
* TanStack's chat loop would swallow it into an
|
|
19
|
+
* `Error executing tool: …` result the model sees instead of the
|
|
20
|
+
* user. Eager mode is unsafe on SSE (see `ConnectConsentMode`), so
|
|
21
|
+
* SSE configs keep the MCP-spec 401 flow.
|
|
22
|
+
* - {@link getConsentChallenge} looks through `MCPConnectionError`
|
|
23
|
+
* (and any other wrapper) `cause` chains so the route handler can
|
|
24
|
+
* redirect without knowing the wrapping shape.
|
|
25
|
+
*
|
|
26
|
+
* `connectAuthProvider` is re-exported unchanged from
|
|
27
|
+
* `@vercel/connect/mcp` for callers who build the transport config
|
|
28
|
+
* themselves; pass `consent: 'eager'` explicitly only with
|
|
29
|
+
* `type: 'http'`.
|
|
30
|
+
*
|
|
31
|
+
* Nothing here imports `@tanstack/ai`, `@tanstack/ai-mcp`,
|
|
32
|
+
* `@ai-sdk/mcp`, or `@modelcontextprotocol/sdk`: the returned provider
|
|
33
|
+
* is the self-contained `ConnectOAuthClientProvider`, which the test
|
|
34
|
+
* suite asserts is assignable to both SDKs' `OAuthClientProvider`
|
|
35
|
+
* interfaces, and the transport config types below mirror
|
|
36
|
+
* `@tanstack/ai-mcp`'s structurally. See
|
|
37
|
+
* `docs/tanstack-ai-mcp-integration.md`.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { chat, toServerSentEventsResponse } from '@tanstack/ai';
|
|
41
|
+
* import { createMCPClient } from '@tanstack/ai-mcp';
|
|
42
|
+
* import {
|
|
43
|
+
* connectMCPTransport,
|
|
44
|
+
* getConsentChallenge,
|
|
45
|
+
* } from '@vercel/connect/tanstack-ai';
|
|
46
|
+
*
|
|
47
|
+
* export async function POST(request: Request) {
|
|
48
|
+
* try {
|
|
49
|
+
* const linear = await createMCPClient({
|
|
50
|
+
* transport: connectMCPTransport(
|
|
51
|
+
* { type: 'http', url: 'https://mcp.linear.app/mcp' },
|
|
52
|
+
* 'oauth/linear',
|
|
53
|
+
* { subject: { type: 'user', id: userId } },
|
|
54
|
+
* { redirectUrl: 'https://app.example.com/integrations' }
|
|
55
|
+
* ),
|
|
56
|
+
* });
|
|
57
|
+
* const stream = chat({ adapter, messages, mcp: { clients: [linear] } });
|
|
58
|
+
* return toServerSentEventsResponse(stream);
|
|
59
|
+
* } catch (err) {
|
|
60
|
+
* const challenge = getConsentChallenge(err);
|
|
61
|
+
* if (challenge) return Response.redirect(challenge.url);
|
|
62
|
+
* throw err;
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
import { type ConnectAuthProviderOptions, type ConnectOAuthClientProvider } from '../mcp/connect-auth-provider.js';
|
|
68
|
+
import type { ConnectTokenParams } from '../token.js';
|
|
69
|
+
/**
|
|
70
|
+
* The network transport configs accepted by `@tanstack/ai-mcp`'s
|
|
71
|
+
* `createMCPClient` / `createMCPClients`, mirrored structurally so this
|
|
72
|
+
* module needs no TanStack import. `type: 'http'` is Streamable HTTP
|
|
73
|
+
* (preferred, edge-safe); `type: 'sse'` is the legacy SSE transport.
|
|
74
|
+
*/
|
|
75
|
+
export interface ConnectMCPTransportConfig {
|
|
76
|
+
readonly type: 'http' | 'sse';
|
|
77
|
+
readonly url: string;
|
|
78
|
+
/** Extra headers sent with every request (e.g. deployment-protection bypass). */
|
|
79
|
+
readonly headers?: Record<string, string>;
|
|
80
|
+
/** Bring-your-own `fetch`, forwarded to the SDK transport. */
|
|
81
|
+
readonly fetch?: typeof fetch;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* {@link ConnectMCPTransportConfig} with the Connect provider attached
|
|
85
|
+
* and, unless `redirect: 'follow'` was requested, a `fetch` that refuses
|
|
86
|
+
* HTTP redirects.
|
|
87
|
+
*/
|
|
88
|
+
export type ConnectMCPTransport<T extends ConnectMCPTransportConfig> = T & {
|
|
89
|
+
readonly fetch?: typeof fetch;
|
|
90
|
+
readonly authProvider: ConnectOAuthClientProvider;
|
|
91
|
+
};
|
|
92
|
+
/** Options accepted by {@link connectMCPTransport}. */
|
|
93
|
+
export interface ConnectMCPTransportOptions extends ConnectAuthProviderOptions {
|
|
94
|
+
/**
|
|
95
|
+
* How HTTP redirects from the MCP server are handled, for every
|
|
96
|
+
* request the transport makes (`initialize`, tool calls, the SSE
|
|
97
|
+
* stream, session teardown).
|
|
98
|
+
*
|
|
99
|
+
* - `'error'` (default): the request fails instead of following the
|
|
100
|
+
* redirect. The official SDK otherwise uses native fetch's
|
|
101
|
+
* `'follow'`, which lets a compromised MCP server bounce the
|
|
102
|
+
* application to private-network URLs (a blind GET; the Fetch
|
|
103
|
+
* spec strips `Authorization` on cross-origin redirects). This
|
|
104
|
+
* matches `@ai-sdk/mcp`, whose transport also defaults to
|
|
105
|
+
* `'error'`.
|
|
106
|
+
* - `'follow'`: native behavior, for an endpoint that legitimately
|
|
107
|
+
* redirects. `config.fetch` is then passed through untouched.
|
|
108
|
+
*
|
|
109
|
+
* The policy is applied by wrapping `config.fetch` (or the global
|
|
110
|
+
* `fetch`, resolved at call time), so a caller-supplied guarded
|
|
111
|
+
* fetch keeps its own protections. On `type: 'http'` the wrapper
|
|
112
|
+
* passes fetch's own `redirect: 'error'`, which rejects the request.
|
|
113
|
+
* On `type: 'sse'` it passes `redirect: 'manual'` instead: the
|
|
114
|
+
* official SDK's `SSEClientTransport` runs `fetch` inside an
|
|
115
|
+
* EventSource that treats any rejection as a retryable network
|
|
116
|
+
* error and reconnects every few seconds with no handle left for
|
|
117
|
+
* the caller to close, whereas a non-200 response is terminal. With
|
|
118
|
+
* `'manual'` the 3xx comes back as that terminal response, so the
|
|
119
|
+
* connection fails once and stays closed.
|
|
120
|
+
*/
|
|
121
|
+
readonly redirect?: 'follow' | 'error';
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Builds a `@tanstack/ai-mcp` transport config whose `authProvider` is
|
|
125
|
+
* backed by Vercel Connect, choosing the consent mode that is safe for
|
|
126
|
+
* the transport: `'eager'` for `type: 'http'` (consent surfaces inside
|
|
127
|
+
* `createMCPClient`, before any request reaches the server) and
|
|
128
|
+
* `'transport'` for `type: 'sse'` (the MCP-spec 401 flow; eager mode
|
|
129
|
+
* would be flattened into a retry loop by the SSE EventSource). An
|
|
130
|
+
* explicit `options.consent` overrides the choice — only do that with
|
|
131
|
+
* `type: 'http'`.
|
|
132
|
+
*
|
|
133
|
+
* `headers` on `config` are passed through untouched. `fetch` is
|
|
134
|
+
* wrapped to refuse HTTP redirects unless `options.redirect` is
|
|
135
|
+
* `'follow'`; see {@link ConnectMCPTransportOptions.redirect}.
|
|
136
|
+
*
|
|
137
|
+
* @param config TanStack transport config without `authProvider`.
|
|
138
|
+
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
139
|
+
* or opaque connector id.
|
|
140
|
+
* @param params Token request parameters — always specify a `subject`.
|
|
141
|
+
* @param options Optional adapter and transport behavior overrides.
|
|
142
|
+
*/
|
|
143
|
+
export declare function connectMCPTransport<T extends ConnectMCPTransportConfig>(config: T, connector: string, params: ConnectTokenParams, options?: ConnectMCPTransportOptions): ConnectMCPTransport<T>;
|
|
144
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, type ConnectAuthProviderOptions, type ConnectConsentMode, type ConnectOAuthClientInformation, type ConnectOAuthClientMetadata, type ConnectOAuthClientProvider, type ConnectOAuthDiscoveryState, type ConnectOAuthTokens, type ConsentChallenge, } from '../mcp/connect-auth-provider.js';
|
|
145
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, type ConnectErrorOptions, type ConnectTokenExchangeSubject, type ConnectTokenParams, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from '../token.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/tanstack-ai` subpath.
|
|
3
|
+
*
|
|
4
|
+
* [TanStack AI](https://tanstack.com/ai)'s MCP client
|
|
5
|
+
* (`@tanstack/ai-mcp`) is built directly on the official
|
|
6
|
+
* `@modelcontextprotocol/sdk` transports and accepts any MCP-spec
|
|
7
|
+
* `OAuthClientProvider` on its `http` / `sse` transport config. This
|
|
8
|
+
* subpath adds one TanStack-shaped helper on top of the shared Connect
|
|
9
|
+
* adapter:
|
|
10
|
+
*
|
|
11
|
+
* - {@link connectMCPTransport} builds the whole
|
|
12
|
+
* `createMCPClient({ transport })` config and picks the consent mode
|
|
13
|
+
* per transport type: `'eager'` for Streamable HTTP, `'transport'`
|
|
14
|
+
* for SSE. Eager consent makes `tokens()` raise the Connect consent
|
|
15
|
+
* challenge before the first request (the MCP `initialize`
|
|
16
|
+
* handshake) leaves the process, so a missing grant always fails at
|
|
17
|
+
* the route boundary, never mid-run inside a tool call where
|
|
18
|
+
* TanStack's chat loop would swallow it into an
|
|
19
|
+
* `Error executing tool: …` result the model sees instead of the
|
|
20
|
+
* user. Eager mode is unsafe on SSE (see `ConnectConsentMode`), so
|
|
21
|
+
* SSE configs keep the MCP-spec 401 flow.
|
|
22
|
+
* - {@link getConsentChallenge} looks through `MCPConnectionError`
|
|
23
|
+
* (and any other wrapper) `cause` chains so the route handler can
|
|
24
|
+
* redirect without knowing the wrapping shape.
|
|
25
|
+
*
|
|
26
|
+
* `connectAuthProvider` is re-exported unchanged from
|
|
27
|
+
* `@vercel/connect/mcp` for callers who build the transport config
|
|
28
|
+
* themselves; pass `consent: 'eager'` explicitly only with
|
|
29
|
+
* `type: 'http'`.
|
|
30
|
+
*
|
|
31
|
+
* Nothing here imports `@tanstack/ai`, `@tanstack/ai-mcp`,
|
|
32
|
+
* `@ai-sdk/mcp`, or `@modelcontextprotocol/sdk`: the returned provider
|
|
33
|
+
* is the self-contained `ConnectOAuthClientProvider`, which the test
|
|
34
|
+
* suite asserts is assignable to both SDKs' `OAuthClientProvider`
|
|
35
|
+
* interfaces, and the transport config types below mirror
|
|
36
|
+
* `@tanstack/ai-mcp`'s structurally. See
|
|
37
|
+
* `docs/tanstack-ai-mcp-integration.md`.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { chat, toServerSentEventsResponse } from '@tanstack/ai';
|
|
41
|
+
* import { createMCPClient } from '@tanstack/ai-mcp';
|
|
42
|
+
* import {
|
|
43
|
+
* connectMCPTransport,
|
|
44
|
+
* getConsentChallenge,
|
|
45
|
+
* } from '@vercel/connect/tanstack-ai';
|
|
46
|
+
*
|
|
47
|
+
* export async function POST(request: Request) {
|
|
48
|
+
* try {
|
|
49
|
+
* const linear = await createMCPClient({
|
|
50
|
+
* transport: connectMCPTransport(
|
|
51
|
+
* { type: 'http', url: 'https://mcp.linear.app/mcp' },
|
|
52
|
+
* 'oauth/linear',
|
|
53
|
+
* { subject: { type: 'user', id: userId } },
|
|
54
|
+
* { redirectUrl: 'https://app.example.com/integrations' }
|
|
55
|
+
* ),
|
|
56
|
+
* });
|
|
57
|
+
* const stream = chat({ adapter, messages, mcp: { clients: [linear] } });
|
|
58
|
+
* return toServerSentEventsResponse(stream);
|
|
59
|
+
* } catch (err) {
|
|
60
|
+
* const challenge = getConsentChallenge(err);
|
|
61
|
+
* if (challenge) return Response.redirect(challenge.url);
|
|
62
|
+
* throw err;
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
import { connectAuthProvider, } from '../mcp/connect-auth-provider.js';
|
|
68
|
+
/**
|
|
69
|
+
* Builds a `@tanstack/ai-mcp` transport config whose `authProvider` is
|
|
70
|
+
* backed by Vercel Connect, choosing the consent mode that is safe for
|
|
71
|
+
* the transport: `'eager'` for `type: 'http'` (consent surfaces inside
|
|
72
|
+
* `createMCPClient`, before any request reaches the server) and
|
|
73
|
+
* `'transport'` for `type: 'sse'` (the MCP-spec 401 flow; eager mode
|
|
74
|
+
* would be flattened into a retry loop by the SSE EventSource). An
|
|
75
|
+
* explicit `options.consent` overrides the choice — only do that with
|
|
76
|
+
* `type: 'http'`.
|
|
77
|
+
*
|
|
78
|
+
* `headers` on `config` are passed through untouched. `fetch` is
|
|
79
|
+
* wrapped to refuse HTTP redirects unless `options.redirect` is
|
|
80
|
+
* `'follow'`; see {@link ConnectMCPTransportOptions.redirect}.
|
|
81
|
+
*
|
|
82
|
+
* @param config TanStack transport config without `authProvider`.
|
|
83
|
+
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
84
|
+
* or opaque connector id.
|
|
85
|
+
* @param params Token request parameters — always specify a `subject`.
|
|
86
|
+
* @param options Optional adapter and transport behavior overrides.
|
|
87
|
+
*/
|
|
88
|
+
export function connectMCPTransport(config, connector, params, options) {
|
|
89
|
+
const { redirect = 'error', ...providerOptions } = options ?? {};
|
|
90
|
+
const consent = providerOptions.consent ?? (config.type === 'http' ? 'eager' : 'transport');
|
|
91
|
+
return {
|
|
92
|
+
...config,
|
|
93
|
+
...(redirect === 'error' && {
|
|
94
|
+
fetch: withoutRedirects(config.fetch, config.type),
|
|
95
|
+
}),
|
|
96
|
+
authProvider: connectAuthProvider(connector, params, {
|
|
97
|
+
...providerOptions,
|
|
98
|
+
consent,
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Wraps `fetchImpl` (or the global `fetch`, looked up per call so
|
|
104
|
+
* request-scoped fetch shims keep working) so no request follows a
|
|
105
|
+
* server-chosen `Location`.
|
|
106
|
+
*
|
|
107
|
+
* Streamable HTTP gets `redirect: 'error'`: fetch rejects the 3xx with
|
|
108
|
+
* a `TypeError` and the SDK surfaces a connection error. SSE gets
|
|
109
|
+
* `redirect: 'manual'`: fetch resolves with the 3xx itself (status
|
|
110
|
+
* `303`, or `0` on runtimes that return an opaque redirect), which the
|
|
111
|
+
* SDK's EventSource treats as a terminal non-200 response. A rejection
|
|
112
|
+
* there would instead be treated as a transient network error and
|
|
113
|
+
* retried on a timer that outlives the failed `createMCPClient` call.
|
|
114
|
+
*/
|
|
115
|
+
function withoutRedirects(fetchImpl, transportType) {
|
|
116
|
+
const redirect = transportType === 'sse' ? 'manual' : 'error';
|
|
117
|
+
return (input, init) => (fetchImpl ?? fetch)(input, { ...init, redirect });
|
|
118
|
+
}
|
|
119
|
+
export { CONNECT_DISCOVERY_STATE, CONNECT_MANAGED_REDIRECT_URL, connectAuthProvider, ConsentRequiredError, getConsentChallenge, isConsentRequiredError, } from '../mcp/connect-auth-provider.js';
|
|
120
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/connect",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"./ai-sdk": {
|
|
37
37
|
"types": "./dist/ai-sdk/index.d.ts",
|
|
38
38
|
"default": "./dist/ai-sdk/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./tanstack-ai": {
|
|
41
|
+
"types": "./dist/tanstack-ai/index.d.ts",
|
|
42
|
+
"default": "./dist/tanstack-ai/index.js"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"files": [
|
|
@@ -48,9 +52,10 @@
|
|
|
48
52
|
"@ai-sdk/mcp": "^1 || ^2",
|
|
49
53
|
"@auth/core": ">=0.37.0",
|
|
50
54
|
"@chat-adapter/slack": "^4.0.0",
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
56
|
"ai": "^6 || ^7.0.0-beta.0",
|
|
52
57
|
"better-auth": ">=1.5.0",
|
|
53
|
-
"eve": ">=0.
|
|
58
|
+
"eve": ">=0.39.1"
|
|
54
59
|
},
|
|
55
60
|
"peerDependenciesMeta": {
|
|
56
61
|
"@ai-sdk/mcp": {
|
|
@@ -62,6 +67,9 @@
|
|
|
62
67
|
"@chat-adapter/slack": {
|
|
63
68
|
"optional": true
|
|
64
69
|
},
|
|
70
|
+
"@modelcontextprotocol/sdk": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
65
73
|
"ai": {
|
|
66
74
|
"optional": true
|
|
67
75
|
},
|
|
@@ -76,6 +84,8 @@
|
|
|
76
84
|
"@ai-sdk/mcp": "2.0.0-beta.37",
|
|
77
85
|
"@auth/core": "0.37.4",
|
|
78
86
|
"@chat-adapter/slack": "4.31.0",
|
|
87
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
88
|
+
"@tanstack/ai-mcp": "0.3.10",
|
|
79
89
|
"ai": "7.0.26",
|
|
80
90
|
"better-auth": "1.5.5",
|
|
81
91
|
"eve": "0.39.1",
|