@opendatalabs/personal-server-ts-core 1.29.1 → 1.30.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/dist/mcp/approval-url.d.ts +54 -0
- package/dist/mcp/approval-url.d.ts.map +1 -0
- package/dist/mcp/approval-url.js +81 -0
- package/dist/mcp/approval-url.js.map +1 -0
- package/dist/mcp/connection-api.d.ts +66 -2
- package/dist/mcp/connection-api.d.ts.map +1 -1
- package/dist/mcp/connection-api.js +228 -0
- package/dist/mcp/connection-api.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +58 -8
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/types.d.ts +16 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/ports/index.d.ts +5 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/sync/workers/download.d.ts +3 -1
- package/dist/sync/workers/download.d.ts.map +1 -1
- package/dist/sync/workers/download.js +16 -5
- package/dist/sync/workers/download.js.map +1 -1
- package/dist/test-utils/memory-storage.d.ts.map +1 -1
- package/dist/test-utils/memory-storage.js +8 -1
- package/dist/test-utils/memory-storage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the owner answers an MCP client's scope request.
|
|
3
|
+
*
|
|
4
|
+
* `request_scope_access` only records a pending request; the owner decides it
|
|
5
|
+
* on Vana Web (the same approval surface as app data connection requests).
|
|
6
|
+
* The page loads the request from this server and posts the decision back to
|
|
7
|
+
* `POST /v1/mcp/connections/:id/scope-request/{approve,deny}` with a
|
|
8
|
+
* Web3Signed owner request, so the link has to name both the connection and
|
|
9
|
+
* the server's public origin:
|
|
10
|
+
*
|
|
11
|
+
* https://app.vana.org/mcp/requests/<connectionId>?ps_origin=<server origin>
|
|
12
|
+
*
|
|
13
|
+
* `ps_origin` is the same param the MCP OAuth approval link carries. The page
|
|
14
|
+
* treats it as a claim, never as proof: before it signs anything it checks
|
|
15
|
+
* that the Gateway lists an active server registered by the signed-in owner
|
|
16
|
+
* at exactly that origin.
|
|
17
|
+
*/
|
|
18
|
+
/** Production Vana Web. */
|
|
19
|
+
export declare const VANA_WEB_ORIGIN = "https://app.vana.org";
|
|
20
|
+
/** Vana Web route that renders one MCP scope request. */
|
|
21
|
+
export declare const MCP_SCOPE_REQUEST_APPROVAL_PATH = "/mcp/requests";
|
|
22
|
+
/** Query param naming the server that holds the request. */
|
|
23
|
+
export declare const MCP_APPROVAL_PS_ORIGIN_PARAM = "ps_origin";
|
|
24
|
+
/** What a host knows when the agent asks for a link. */
|
|
25
|
+
export interface McpScopeRequestApprovalUrlContext {
|
|
26
|
+
/**
|
|
27
|
+
* This server's current public origin (the Web3Signed audience): the
|
|
28
|
+
* tunnel URL once it is up, otherwise the configured origin.
|
|
29
|
+
*/
|
|
30
|
+
serverOrigin?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Host hook: returns the page for one connection's pending request, or
|
|
34
|
+
* undefined to keep pointing the owner at Vana without a link.
|
|
35
|
+
*/
|
|
36
|
+
export type McpScopeRequestApprovalUrlHook = (connectionId: string, context: McpScopeRequestApprovalUrlContext) => string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The approval link for one connection, or undefined when this server cannot
|
|
39
|
+
* be reached from that web origin (no public origin yet, local-only http).
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildMcpScopeRequestApprovalUrl(input: {
|
|
42
|
+
connectionId: string;
|
|
43
|
+
serverOrigin: string | undefined;
|
|
44
|
+
webOrigin?: string;
|
|
45
|
+
}): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Default hook for hosts that let Vana Web answer scope requests: builds the
|
|
48
|
+
* link from the server's current public origin at the moment the agent asks.
|
|
49
|
+
* Pass `webOrigin` for app-dev or a local web dev server.
|
|
50
|
+
*/
|
|
51
|
+
export declare function vanaWebMcpScopeRequestApprovalUrl(options?: {
|
|
52
|
+
webOrigin?: string;
|
|
53
|
+
}): McpScopeRequestApprovalUrlHook;
|
|
54
|
+
//# sourceMappingURL=approval-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-url.d.ts","sourceRoot":"","sources":["../../src/mcp/approval-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,2BAA2B;AAC3B,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD,yDAAyD;AACzD,eAAO,MAAM,+BAA+B,kBAAkB,CAAC;AAE/D,4DAA4D;AAC5D,eAAO,MAAM,4BAA4B,cAAc,CAAC;AAExD,wDAAwD;AACxD,MAAM,WAAW,iCAAiC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,MAAM,8BAA8B,GAAG,CAC3C,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,iCAAiC,KACvC,MAAM,GAAG,SAAS,CAAC;AAkCxB;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,GAAG,SAAS,CAWrB;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,8BAA8B,CAOhC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the owner answers an MCP client's scope request.
|
|
3
|
+
*
|
|
4
|
+
* `request_scope_access` only records a pending request; the owner decides it
|
|
5
|
+
* on Vana Web (the same approval surface as app data connection requests).
|
|
6
|
+
* The page loads the request from this server and posts the decision back to
|
|
7
|
+
* `POST /v1/mcp/connections/:id/scope-request/{approve,deny}` with a
|
|
8
|
+
* Web3Signed owner request, so the link has to name both the connection and
|
|
9
|
+
* the server's public origin:
|
|
10
|
+
*
|
|
11
|
+
* https://app.vana.org/mcp/requests/<connectionId>?ps_origin=<server origin>
|
|
12
|
+
*
|
|
13
|
+
* `ps_origin` is the same param the MCP OAuth approval link carries. The page
|
|
14
|
+
* treats it as a claim, never as proof: before it signs anything it checks
|
|
15
|
+
* that the Gateway lists an active server registered by the signed-in owner
|
|
16
|
+
* at exactly that origin.
|
|
17
|
+
*/
|
|
18
|
+
/** Production Vana Web. */
|
|
19
|
+
export const VANA_WEB_ORIGIN = "https://app.vana.org";
|
|
20
|
+
/** Vana Web route that renders one MCP scope request. */
|
|
21
|
+
export const MCP_SCOPE_REQUEST_APPROVAL_PATH = "/mcp/requests";
|
|
22
|
+
/** Query param naming the server that holds the request. */
|
|
23
|
+
export const MCP_APPROVAL_PS_ORIGIN_PARAM = "ps_origin";
|
|
24
|
+
function parseOrigin(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return undefined;
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
return url.protocol === "http:" || url.protocol === "https:"
|
|
30
|
+
? url
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isLoopbackHost(hostname) {
|
|
38
|
+
return (hostname === "localhost" ||
|
|
39
|
+
hostname === "127.0.0.1" ||
|
|
40
|
+
hostname === "[::1]" ||
|
|
41
|
+
hostname.endsWith(".localhost"));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A page on `webOrigin` can only call a server it can reach: an https server
|
|
45
|
+
* from anywhere, a loopback http server only from a loopback dev web origin
|
|
46
|
+
* (a public https page cannot fetch http, and a phone cannot reach the
|
|
47
|
+
* laptop's loopback at all).
|
|
48
|
+
*/
|
|
49
|
+
function isReachableFromWeb(web, server) {
|
|
50
|
+
if (server.protocol === "https:")
|
|
51
|
+
return true;
|
|
52
|
+
return isLoopbackHost(server.hostname) && isLoopbackHost(web.hostname);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The approval link for one connection, or undefined when this server cannot
|
|
56
|
+
* be reached from that web origin (no public origin yet, local-only http).
|
|
57
|
+
*/
|
|
58
|
+
export function buildMcpScopeRequestApprovalUrl(input) {
|
|
59
|
+
const web = parseOrigin(input.webOrigin ?? VANA_WEB_ORIGIN);
|
|
60
|
+
const server = parseOrigin(input.serverOrigin);
|
|
61
|
+
if (!web || !server || !input.connectionId)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (!isReachableFromWeb(web, server))
|
|
64
|
+
return undefined;
|
|
65
|
+
const url = new URL(`${MCP_SCOPE_REQUEST_APPROVAL_PATH}/${encodeURIComponent(input.connectionId)}`, web.origin);
|
|
66
|
+
url.searchParams.set(MCP_APPROVAL_PS_ORIGIN_PARAM, server.origin);
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Default hook for hosts that let Vana Web answer scope requests: builds the
|
|
71
|
+
* link from the server's current public origin at the moment the agent asks.
|
|
72
|
+
* Pass `webOrigin` for app-dev or a local web dev server.
|
|
73
|
+
*/
|
|
74
|
+
export function vanaWebMcpScopeRequestApprovalUrl(options = {}) {
|
|
75
|
+
return (connectionId, context) => buildMcpScopeRequestApprovalUrl({
|
|
76
|
+
connectionId,
|
|
77
|
+
serverOrigin: context.serverOrigin,
|
|
78
|
+
webOrigin: options.webOrigin,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=approval-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-url.js","sourceRoot":"","sources":["../../src/mcp/approval-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAEtD,yDAAyD;AACzD,MAAM,CAAC,MAAM,+BAA+B,GAAG,eAAe,CAAC;AAE/D,4DAA4D;AAC5D,MAAM,CAAC,MAAM,4BAA4B,GAAG,WAAW,CAAC;AAoBxD,SAAS,WAAW,CAAC,KAAyB;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAC1D,CAAC,CAAC,GAAG;YACL,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB;IACtC,OAAO,CACL,QAAQ,KAAK,WAAW;QACxB,QAAQ,KAAK,WAAW;QACxB,QAAQ,KAAK,OAAO;QACpB,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAChC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,GAAQ,EAAE,MAAW;IAC/C,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,+BAA+B,CAAC,KAI/C;IACC,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,SAAS,IAAI,eAAe,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC/C,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY;QAAE,OAAO,SAAS,CAAC;IAC7D,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IACvD,MAAM,GAAG,GAAG,IAAI,GAAG,CACjB,GAAG,+BAA+B,IAAI,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,EAC9E,GAAG,CAAC,MAAM,CACX,CAAC;IACF,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,4BAA4B,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iCAAiC,CAC/C,UAAkC,EAAE;IAEpC,OAAO,CAAC,YAAY,EAAE,OAAO,EAAE,EAAE,CAC/B,+BAA+B,CAAC;QAC9B,YAAY;QACZ,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* POST /v1/mcp/connections/:id/approve
|
|
8
8
|
* DELETE /v1/mcp/connections/:id
|
|
9
9
|
*
|
|
10
|
+
* plus the server-signed answer to an MCP client's `request_scope_access`:
|
|
11
|
+
*
|
|
12
|
+
* POST /v1/mcp/connections/:id/scope-request/approve
|
|
13
|
+
* POST /v1/mcp/connections/:id/scope-request/deny
|
|
14
|
+
*
|
|
10
15
|
* These endpoints DO NOT read user data. They only manage connection records
|
|
11
16
|
* — create the per-connection grantee/token, store grant ids after the user
|
|
12
17
|
* approves them, and mark connections revoked.
|
|
13
18
|
*/
|
|
14
|
-
import type
|
|
19
|
+
import { type DataPortabilityGatewayConfig, type GatewayClient } from "@opendatalabs/vana-sdk/browser";
|
|
15
20
|
import type { ServerSigner } from "../signing/index.js";
|
|
16
|
-
import type { McpConnectionRecord, McpConnectionStore, McpConnectionGrant, McpScopeAccessRequest, McpOAuthAuthorizationRecord, McpOAuthAuthorizationStore } from "./types.js";
|
|
21
|
+
import type { McpConnectionRecord, McpConnectionStore, McpConnectionGrant, McpScopeAccessDecision, McpScopeAccessRequest, McpOAuthAuthorizationRecord, McpOAuthAuthorizationStore } from "./types.js";
|
|
17
22
|
export declare function hashConnectionToken(token: string): Promise<string>;
|
|
18
23
|
export interface CreateMcpConnectionInput {
|
|
19
24
|
displayName?: string;
|
|
@@ -49,6 +54,13 @@ export interface ApproveMcpConnectionOptions {
|
|
|
49
54
|
store: McpConnectionStore;
|
|
50
55
|
now?: () => Date;
|
|
51
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* errorCode for an unknown connection id on the single-connection and
|
|
59
|
+
* scope-request routes. Distinct from the generic `NOT_FOUND` a server
|
|
60
|
+
* answers for a route it does not have, so a caller (Vana Web) can tell
|
|
61
|
+
* "this request is gone" from "this server predates the route".
|
|
62
|
+
*/
|
|
63
|
+
export declare const MCP_CONNECTION_NOT_FOUND = "MCP_CONNECTION_NOT_FOUND";
|
|
52
64
|
export declare class McpConnectionNotFoundError extends Error {
|
|
53
65
|
connectionId: string;
|
|
54
66
|
constructor(connectionId: string);
|
|
@@ -71,6 +83,51 @@ export declare function requestMcpScopeAccess(input: {
|
|
|
71
83
|
connection: McpConnectionRecord;
|
|
72
84
|
requestRecorded: boolean;
|
|
73
85
|
}>;
|
|
86
|
+
/** A scope-request answer the owner cannot give in the connection's state. */
|
|
87
|
+
export declare class McpScopeRequestError extends Error {
|
|
88
|
+
code: string;
|
|
89
|
+
status: 400 | 409 | 500 | 502;
|
|
90
|
+
body?: unknown | undefined;
|
|
91
|
+
constructor(code: string, message: string, status?: 400 | 409 | 500 | 502, body?: unknown | undefined);
|
|
92
|
+
}
|
|
93
|
+
export interface ApproveMcpScopeAccessRequestInput {
|
|
94
|
+
connectionId: string;
|
|
95
|
+
/** Scopes the owner approves; must be a subset of the pending request. */
|
|
96
|
+
scopes: unknown;
|
|
97
|
+
}
|
|
98
|
+
export interface ApproveMcpScopeAccessRequestOptions {
|
|
99
|
+
store: McpConnectionStore;
|
|
100
|
+
gateway: Pick<GatewayClient, "getBuilder" | "getGrant" | "createGrant">;
|
|
101
|
+
serverOwner?: `0x${string}`;
|
|
102
|
+
serverSigner?: Pick<ServerSigner, "signGrantRegistration">;
|
|
103
|
+
now?: () => Date;
|
|
104
|
+
}
|
|
105
|
+
export interface ApproveMcpScopeAccessRequestOutput {
|
|
106
|
+
connection: McpConnectionRecord;
|
|
107
|
+
grantId: string;
|
|
108
|
+
/** Every scope the connection's grant now covers. */
|
|
109
|
+
grantedScopes: string[];
|
|
110
|
+
approvedScopes: string[];
|
|
111
|
+
deniedScopes: string[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Owner approves (part of) a pending `request_scope_access` without an
|
|
115
|
+
* external signer: the server signs the grant itself, as the OAuth approve
|
|
116
|
+
* path does.
|
|
117
|
+
*
|
|
118
|
+
* The gateway keeps ONE grant per (owner, grantee) and a new registration
|
|
119
|
+
* replaces its scopes, so the grant is re-signed over the union of every
|
|
120
|
+
* scope the connection already holds plus the approved ones. Signing only
|
|
121
|
+
* the new scopes would silently revoke the old ones.
|
|
122
|
+
*/
|
|
123
|
+
export declare function approveMcpScopeAccessRequest(input: ApproveMcpScopeAccessRequestInput, options: ApproveMcpScopeAccessRequestOptions): Promise<ApproveMcpScopeAccessRequestOutput>;
|
|
124
|
+
/** Owner declines the pending request; the decision stays for the client. */
|
|
125
|
+
export declare function denyMcpScopeAccessRequest(input: {
|
|
126
|
+
connectionId: string;
|
|
127
|
+
}, options: {
|
|
128
|
+
store: McpConnectionStore;
|
|
129
|
+
now?: () => Date;
|
|
130
|
+
}): Promise<McpConnectionRecord>;
|
|
74
131
|
export interface RevokeMcpConnectionOptions {
|
|
75
132
|
store: McpConnectionStore;
|
|
76
133
|
now?: () => Date;
|
|
@@ -86,13 +143,20 @@ export interface McpConnectionView {
|
|
|
86
143
|
granteeAddress: `0x${string}`;
|
|
87
144
|
status: "pending" | "approved" | "revoked";
|
|
88
145
|
grants: McpConnectionGrant[];
|
|
146
|
+
/** Every scope the connection's grants cover, deduplicated and sorted. */
|
|
147
|
+
grantedScopes: string[];
|
|
148
|
+
/** Scopes the MCP client is still asking for, if any. */
|
|
89
149
|
scopeAccessRequest?: McpScopeAccessRequest;
|
|
150
|
+
/** Owner's answer to the most recent scope request. */
|
|
151
|
+
scopeAccessDecision?: McpScopeAccessDecision;
|
|
90
152
|
createdAt: string;
|
|
91
153
|
approvedAt?: string;
|
|
92
154
|
revokedAt?: string;
|
|
93
155
|
lastUsedAt?: string;
|
|
94
156
|
}
|
|
95
157
|
export declare function toMcpConnectionView(record: McpConnectionRecord): McpConnectionView;
|
|
158
|
+
/** One connection as the owner sees it; throws when the id is unknown. */
|
|
159
|
+
export declare function getMcpConnectionView(connectionId: string, store: McpConnectionStore): Promise<McpConnectionView>;
|
|
96
160
|
export declare function listMcpConnectionViews(store: McpConnectionStore): Promise<McpConnectionView[]>;
|
|
97
161
|
export interface CreateMcpOAuthAuthorizationInput {
|
|
98
162
|
clientId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-api.d.ts","sourceRoot":"","sources":["../../src/mcp/connection-api.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connection-api.d.ts","sourceRoot":"","sources":["../../src/mcp/connection-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* POST /v1/mcp/connections/:id/approve
|
|
8
8
|
* DELETE /v1/mcp/connections/:id
|
|
9
9
|
*
|
|
10
|
+
* plus the server-signed answer to an MCP client's `request_scope_access`:
|
|
11
|
+
*
|
|
12
|
+
* POST /v1/mcp/connections/:id/scope-request/approve
|
|
13
|
+
* POST /v1/mcp/connections/:id/scope-request/deny
|
|
14
|
+
*
|
|
10
15
|
* These endpoints DO NOT read user data. They only manage connection records
|
|
11
16
|
* — create the per-connection grantee/token, store grant ids after the user
|
|
12
17
|
* approves them, and mark connections revoked.
|
|
13
18
|
*/
|
|
14
19
|
import { generateMcpGrantee } from "./grantee.js";
|
|
15
20
|
import { createGrantContract } from "../contracts/index.js";
|
|
21
|
+
import { ScopeSchema, } from "@opendatalabs/vana-sdk/browser";
|
|
16
22
|
import { appUrlFromOAuthRedirectUri, ensureMcpGranteeRegistered, McpGranteeRegistrationError, } from "./builder-registration.js";
|
|
17
23
|
import { MCP_SCOPE_ACCESS_REQUEST_LIMIT } from "./types.js";
|
|
18
24
|
import { isMcpRefreshExpired, MCP_TOKEN_TTL_MS, mcpRefreshExpiry, mcpTokenExpiry, } from "./token-expiry.js";
|
|
@@ -103,6 +109,13 @@ export function buildMcpProtectedResourceMetadataUrl(publicOrigin) {
|
|
|
103
109
|
: publicOrigin;
|
|
104
110
|
return `${base}/.well-known/oauth-protected-resource/mcp`;
|
|
105
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* errorCode for an unknown connection id on the single-connection and
|
|
114
|
+
* scope-request routes. Distinct from the generic `NOT_FOUND` a server
|
|
115
|
+
* answers for a route it does not have, so a caller (Vana Web) can tell
|
|
116
|
+
* "this request is gone" from "this server predates the route".
|
|
117
|
+
*/
|
|
118
|
+
export const MCP_CONNECTION_NOT_FOUND = "MCP_CONNECTION_NOT_FOUND";
|
|
106
119
|
export class McpConnectionNotFoundError extends Error {
|
|
107
120
|
connectionId;
|
|
108
121
|
constructor(connectionId) {
|
|
@@ -185,6 +198,212 @@ export async function requestMcpScopeAccess(input, options) {
|
|
|
185
198
|
missingInputScopes.every((scope) => updated.scopeAccessRequest?.scopes.includes(scope)),
|
|
186
199
|
};
|
|
187
200
|
}
|
|
201
|
+
/** A scope-request answer the owner cannot give in the connection's state. */
|
|
202
|
+
export class McpScopeRequestError extends Error {
|
|
203
|
+
code;
|
|
204
|
+
status;
|
|
205
|
+
body;
|
|
206
|
+
constructor(code, message, status = 400, body) {
|
|
207
|
+
super(message);
|
|
208
|
+
this.code = code;
|
|
209
|
+
this.status = status;
|
|
210
|
+
this.body = body;
|
|
211
|
+
this.name = "McpScopeRequestError";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Owner approves (part of) a pending `request_scope_access` without an
|
|
216
|
+
* external signer: the server signs the grant itself, as the OAuth approve
|
|
217
|
+
* path does.
|
|
218
|
+
*
|
|
219
|
+
* The gateway keeps ONE grant per (owner, grantee) and a new registration
|
|
220
|
+
* replaces its scopes, so the grant is re-signed over the union of every
|
|
221
|
+
* scope the connection already holds plus the approved ones. Signing only
|
|
222
|
+
* the new scopes would silently revoke the old ones.
|
|
223
|
+
*/
|
|
224
|
+
export async function approveMcpScopeAccessRequest(input, options) {
|
|
225
|
+
const connection = await options.store.getById(input.connectionId);
|
|
226
|
+
if (!connection)
|
|
227
|
+
throw new McpConnectionNotFoundError(input.connectionId);
|
|
228
|
+
const pending = requirePendingScopeRequest(connection);
|
|
229
|
+
const approvedScopes = parseApprovedScopes(input.scopes, pending);
|
|
230
|
+
const live = await readLiveGrantState(connection, options.gateway);
|
|
231
|
+
const grantedScopes = sortedUnique([...live.scopes, ...approvedScopes]);
|
|
232
|
+
let grantResult;
|
|
233
|
+
try {
|
|
234
|
+
grantResult = await createGrantContract({
|
|
235
|
+
gateway: options.gateway,
|
|
236
|
+
serverOwner: options.serverOwner,
|
|
237
|
+
serverSigner: options.serverSigner,
|
|
238
|
+
body: {
|
|
239
|
+
granteeAddress: connection.granteeAddress,
|
|
240
|
+
scopes: grantedScopes,
|
|
241
|
+
grantVersion: live.nextGrantVersion,
|
|
242
|
+
...(live.expiresAt !== undefined ? { expiresAt: live.expiresAt } : {}),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
throw new McpScopeRequestError("GRANT_CREATION_FAILED", err instanceof Error ? err.message : String(err), 502);
|
|
248
|
+
}
|
|
249
|
+
if (!grantResult.ok) {
|
|
250
|
+
throw new McpScopeRequestError("GRANT_CREATION_FAILED", extractGrantErrorMessage(grantResult.body), grantResult.status >= 500 ? 502 : grantResult.status === 409 ? 409 : 400, grantResult.body);
|
|
251
|
+
}
|
|
252
|
+
const grantId = grantResult.body.grantId;
|
|
253
|
+
if (!grantId) {
|
|
254
|
+
throw new McpScopeRequestError("GRANT_CREATION_FAILED", "Gateway did not return a grant id.", 502, grantResult.body);
|
|
255
|
+
}
|
|
256
|
+
const decidedAt = nowIso(options.now);
|
|
257
|
+
const deniedScopes = pending.scopes.filter((scope) => !approvedScopes.includes(scope));
|
|
258
|
+
const updated = await options.store.mutate(input.connectionId, (latest) => {
|
|
259
|
+
// The grant above was signed from the state read before the gateway
|
|
260
|
+
// round trip; refuse to store it over a change made in between.
|
|
261
|
+
requirePendingScopeRequest(latest);
|
|
262
|
+
if (JSON.stringify(latest.grants) !== JSON.stringify(connection.grants)) {
|
|
263
|
+
throw new McpScopeRequestError("CONCURRENT_UPDATE", "The connection's grants changed during approval; retry.", 409);
|
|
264
|
+
}
|
|
265
|
+
const decision = {
|
|
266
|
+
decision: "approved",
|
|
267
|
+
approvedScopes,
|
|
268
|
+
deniedScopes,
|
|
269
|
+
requestedAt: pending.requestedAt,
|
|
270
|
+
decidedAt,
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
...latest,
|
|
274
|
+
grants: [{ grantId, scopes: grantedScopes }],
|
|
275
|
+
scopeAccessRequest: undefined,
|
|
276
|
+
scopeAccessDecision: decision,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
if (!updated)
|
|
280
|
+
throw new McpConnectionNotFoundError(input.connectionId);
|
|
281
|
+
return {
|
|
282
|
+
connection: updated,
|
|
283
|
+
grantId,
|
|
284
|
+
grantedScopes,
|
|
285
|
+
approvedScopes,
|
|
286
|
+
deniedScopes,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** Owner declines the pending request; the decision stays for the client. */
|
|
290
|
+
export async function denyMcpScopeAccessRequest(input, options) {
|
|
291
|
+
const decidedAt = nowIso(options.now);
|
|
292
|
+
const updated = await options.store.mutate(input.connectionId, (latest) => {
|
|
293
|
+
const pending = requirePendingScopeRequest(latest);
|
|
294
|
+
const decision = {
|
|
295
|
+
decision: "denied",
|
|
296
|
+
approvedScopes: [],
|
|
297
|
+
deniedScopes: pending.scopes,
|
|
298
|
+
requestedAt: pending.requestedAt,
|
|
299
|
+
decidedAt,
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
...latest,
|
|
303
|
+
scopeAccessRequest: undefined,
|
|
304
|
+
scopeAccessDecision: decision,
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
if (!updated)
|
|
308
|
+
throw new McpConnectionNotFoundError(input.connectionId);
|
|
309
|
+
return updated;
|
|
310
|
+
}
|
|
311
|
+
function requirePendingScopeRequest(connection) {
|
|
312
|
+
if (connection.status !== "approved") {
|
|
313
|
+
throw new McpConnectionStateError(connection.id, connection.status, "approved");
|
|
314
|
+
}
|
|
315
|
+
const pending = connection.scopeAccessRequest;
|
|
316
|
+
if (!pending || pending.scopes.length === 0) {
|
|
317
|
+
throw new McpScopeRequestError("NO_PENDING_REQUEST", `mcp connection ${connection.id} has no pending scope request`, 409);
|
|
318
|
+
}
|
|
319
|
+
return pending;
|
|
320
|
+
}
|
|
321
|
+
function parseApprovedScopes(value, pending) {
|
|
322
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
323
|
+
throw new McpScopeRequestError("SCOPES_REQUIRED", "Approve requires a non-empty scopes array; use deny to decline all.");
|
|
324
|
+
}
|
|
325
|
+
const scopes = [];
|
|
326
|
+
for (const raw of value) {
|
|
327
|
+
const scope = typeof raw === "string" ? raw.trim() : "";
|
|
328
|
+
if (!ScopeSchema.safeParse(scope).success) {
|
|
329
|
+
throw new McpScopeRequestError("INVALID_SCOPE", `Invalid scope: ${JSON.stringify(raw)}`);
|
|
330
|
+
}
|
|
331
|
+
if (!pending.scopes.includes(scope)) {
|
|
332
|
+
throw new McpScopeRequestError("SCOPE_NOT_REQUESTED", `Scope ${scope} is not part of the pending request`);
|
|
333
|
+
}
|
|
334
|
+
if (!scopes.includes(scope))
|
|
335
|
+
scopes.push(scope);
|
|
336
|
+
}
|
|
337
|
+
return scopes.sort();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Read what the gateway holds for this connection's grantee: the scopes the
|
|
341
|
+
* replacement grant must keep, the version counter it must advance past, and
|
|
342
|
+
* the expiry it must not outlive. A revoked grant contributes its version but
|
|
343
|
+
* no scopes, so approval never resurrects access the owner already pulled.
|
|
344
|
+
*/
|
|
345
|
+
async function readLiveGrantState(connection, gateway) {
|
|
346
|
+
const grantIds = sortedUnique(connection.grants.map((grant) => grant.grantId));
|
|
347
|
+
const revoked = new Set();
|
|
348
|
+
const scopes = new Set();
|
|
349
|
+
let maxVersion = 0n;
|
|
350
|
+
let expiresAt;
|
|
351
|
+
for (const grantId of grantIds) {
|
|
352
|
+
let live;
|
|
353
|
+
try {
|
|
354
|
+
live = await gateway.getGrant(grantId);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
throw new McpScopeRequestError("GATEWAY_UNAVAILABLE", `Could not read grant ${grantId}: ${err instanceof Error ? err.message : String(err)}`, 502);
|
|
358
|
+
}
|
|
359
|
+
if (!live)
|
|
360
|
+
continue;
|
|
361
|
+
const version = parseUint(live.grantVersion);
|
|
362
|
+
if (version !== null && version > maxVersion)
|
|
363
|
+
maxVersion = version;
|
|
364
|
+
if (live.revokedAt) {
|
|
365
|
+
revoked.add(grantId);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
for (const scope of live.scopes)
|
|
369
|
+
scopes.add(scope);
|
|
370
|
+
const liveExpiry = parseExpirySeconds(live.expiresAt);
|
|
371
|
+
if (liveExpiry !== undefined) {
|
|
372
|
+
expiresAt =
|
|
373
|
+
expiresAt === undefined ? liveExpiry : Math.max(expiresAt, liveExpiry);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const grant of connection.grants) {
|
|
377
|
+
if (revoked.has(grant.grantId))
|
|
378
|
+
continue;
|
|
379
|
+
for (const scope of grant.scopes)
|
|
380
|
+
scopes.add(scope);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
scopes: sortedUnique(scopes),
|
|
384
|
+
nextGrantVersion: (maxVersion + 1n).toString(),
|
|
385
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function parseUint(value) {
|
|
389
|
+
if (typeof value !== "string" || !/^\d+$/u.test(value))
|
|
390
|
+
return null;
|
|
391
|
+
return BigInt(value);
|
|
392
|
+
}
|
|
393
|
+
/** Gateway expiry may be unix seconds or an ISO date; 0/null means perpetual. */
|
|
394
|
+
function parseExpirySeconds(value) {
|
|
395
|
+
if (!value)
|
|
396
|
+
return undefined;
|
|
397
|
+
if (/^\d+$/u.test(value)) {
|
|
398
|
+
const seconds = Number(value);
|
|
399
|
+
return seconds > 0 ? seconds : undefined;
|
|
400
|
+
}
|
|
401
|
+
const ms = Date.parse(value);
|
|
402
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1000) : undefined;
|
|
403
|
+
}
|
|
404
|
+
function sortedUnique(values) {
|
|
405
|
+
return Array.from(new Set(values)).sort();
|
|
406
|
+
}
|
|
188
407
|
export async function revokeMcpConnection(connectionId, options) {
|
|
189
408
|
const existing = await options.store.getById(connectionId);
|
|
190
409
|
if (!existing)
|
|
@@ -210,13 +429,22 @@ export function toMcpConnectionView(record) {
|
|
|
210
429
|
granteeAddress: record.granteeAddress,
|
|
211
430
|
status: record.status,
|
|
212
431
|
grants: record.grants,
|
|
432
|
+
grantedScopes: sortedUnique(record.grants.flatMap((grant) => grant.scopes)),
|
|
213
433
|
scopeAccessRequest: record.scopeAccessRequest,
|
|
434
|
+
scopeAccessDecision: record.scopeAccessDecision,
|
|
214
435
|
createdAt: record.createdAt,
|
|
215
436
|
approvedAt: record.approvedAt,
|
|
216
437
|
revokedAt: record.revokedAt,
|
|
217
438
|
lastUsedAt: record.lastUsedAt,
|
|
218
439
|
};
|
|
219
440
|
}
|
|
441
|
+
/** One connection as the owner sees it; throws when the id is unknown. */
|
|
442
|
+
export async function getMcpConnectionView(connectionId, store) {
|
|
443
|
+
const record = await store.getById(connectionId);
|
|
444
|
+
if (!record)
|
|
445
|
+
throw new McpConnectionNotFoundError(connectionId);
|
|
446
|
+
return toMcpConnectionView(record);
|
|
447
|
+
}
|
|
220
448
|
export async function listMcpConnectionViews(store) {
|
|
221
449
|
const records = await store.list();
|
|
222
450
|
return records.map(toMcpConnectionView);
|