@graphorin/mcp 0.6.1 → 0.7.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +64 -3
  3. package/dist/client/adapt-result.d.ts +9 -1
  4. package/dist/client/adapt-result.d.ts.map +1 -1
  5. package/dist/client/adapt-result.js +28 -10
  6. package/dist/client/adapt-result.js.map +1 -1
  7. package/dist/client/client-handlers.js +7 -1
  8. package/dist/client/client-handlers.js.map +1 -1
  9. package/dist/client/client.d.ts.map +1 -1
  10. package/dist/client/client.js +45 -70
  11. package/dist/client/client.js.map +1 -1
  12. package/dist/client/inbound-filters.js +101 -1
  13. package/dist/client/inbound-filters.js.map +1 -1
  14. package/dist/client/index.d.ts +2 -1
  15. package/dist/client/index.js +2 -1
  16. package/dist/client/managed.d.ts +35 -0
  17. package/dist/client/managed.d.ts.map +1 -0
  18. package/dist/client/managed.js +136 -0
  19. package/dist/client/managed.js.map +1 -0
  20. package/dist/client/mcp-resource-reader.js +1 -1
  21. package/dist/client/mcp-resource-reader.js.map +1 -1
  22. package/dist/client/to-tools-run.js +119 -0
  23. package/dist/client/to-tools-run.js.map +1 -0
  24. package/dist/client/to-tools.d.ts +8 -0
  25. package/dist/client/to-tools.d.ts.map +1 -1
  26. package/dist/client/to-tools.js +27 -4
  27. package/dist/client/to-tools.js.map +1 -1
  28. package/dist/client/types.d.ts +12 -3
  29. package/dist/client/types.d.ts.map +1 -1
  30. package/dist/errors/index.d.ts +3 -3
  31. package/dist/errors/index.js +1 -1
  32. package/dist/errors/index.js.map +1 -1
  33. package/dist/helpers/identity.d.ts +11 -0
  34. package/dist/helpers/identity.d.ts.map +1 -1
  35. package/dist/helpers/identity.js +13 -2
  36. package/dist/helpers/identity.js.map +1 -1
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/package.js +1 -1
  42. package/dist/package.js.map +1 -1
  43. package/dist/registry/json-schema.js +26 -8
  44. package/dist/registry/json-schema.js.map +1 -1
  45. package/dist/transport/types.d.ts +9 -0
  46. package/package.json +13 -12
  47. package/src/client/adapt-result.ts +302 -0
  48. package/src/client/client-handlers.ts +215 -0
  49. package/src/client/client.ts +725 -0
  50. package/src/client/defer-loading.ts +108 -0
  51. package/src/client/inbound-filters.ts +246 -0
  52. package/src/client/index.ts +42 -0
  53. package/src/client/managed.ts +222 -0
  54. package/src/client/mcp-resource-reader.ts +183 -0
  55. package/src/client/pinning.ts +48 -0
  56. package/src/client/to-tools-run.ts +178 -0
  57. package/src/client/to-tools.ts +294 -0
  58. package/src/client/transport-factory.ts +117 -0
  59. package/src/client/types.ts +422 -0
  60. package/src/errors/index.ts +170 -0
  61. package/src/helpers/identity.ts +128 -0
  62. package/src/helpers/index.ts +8 -0
  63. package/src/helpers/validate-config.ts +95 -0
  64. package/src/index.ts +49 -0
  65. package/src/oauth/bridge.ts +143 -0
  66. package/src/oauth/index.ts +18 -0
  67. package/src/oauth/library.ts +61 -0
  68. package/src/registry/json-schema.ts +402 -0
  69. package/src/transport/index.ts +15 -0
  70. package/src/transport/types.ts +108 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Helpers for deriving stable {@link ServerIdentity} records from the
3
+ * supplied {@link MCPTransportConfig}, plus the operator-facing
4
+ * formatter the audit emitter and the trace span attributes use.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import { createHash } from 'node:crypto';
10
+
11
+ import type {
12
+ MCPTransportConfig,
13
+ ServerIdentity,
14
+ SseTransportConfig,
15
+ StdioTransportConfig,
16
+ StreamableHttpTransportConfig,
17
+ } from '../transport/types.js';
18
+
19
+ /**
20
+ * Compute the canonical {@link ServerIdentity} for the supplied
21
+ * transport. The id is suitable for use as a registry key and as the
22
+ * operator-facing label in audit rows + trace attributes.
23
+ *
24
+ * W-016: the id derives ONLY from operator-controlled data (the
25
+ * transport config) plus the optional operator-supplied
26
+ * `serverInfoName` override. The name a server self-reports on
27
+ * `initialize` never participates: every security-relevant surface
28
+ * keys off this id (TOFU pins, `mcp:<id>:<uri>` handle scoping, taint
29
+ * labels, audit rows), and a server-controlled id let a rug-pull
30
+ * server mint a fresh TOFU record by renaming itself, or impersonate a
31
+ * trusted server's scope by claiming its name. HTTP-family ids include
32
+ * a non-default port, so localhost:3001 and localhost:3002 are
33
+ * distinct servers.
34
+ *
35
+ * @stable
36
+ */
37
+ export function deriveServerIdentity(
38
+ transport: MCPTransportConfig,
39
+ serverInfoName?: string,
40
+ ): ServerIdentity {
41
+ switch (transport.kind) {
42
+ case 'stdio':
43
+ return deriveStdioIdentity(transport, serverInfoName);
44
+ case 'streamable-http':
45
+ return deriveStreamableHttpIdentity(transport, serverInfoName);
46
+ case 'sse':
47
+ return deriveSseIdentity(transport, serverInfoName);
48
+ }
49
+ }
50
+
51
+ function deriveStdioIdentity(
52
+ transport: StdioTransportConfig,
53
+ serverInfoName?: string,
54
+ ): ServerIdentity {
55
+ const argsHash = createHash('sha256')
56
+ .update((transport.args ?? []).join('\u0000'))
57
+ .digest('hex')
58
+ .slice(0, 16);
59
+ const command = basenameWithoutExt(transport.command);
60
+ const id = serverInfoName ?? `${command}-${argsHash}`;
61
+ return Object.freeze({
62
+ kind: 'mcp-stdio',
63
+ id,
64
+ command,
65
+ argsHash,
66
+ ...(serverInfoName === undefined ? {} : { serverInfoName }),
67
+ });
68
+ }
69
+
70
+ function deriveStreamableHttpIdentity(
71
+ transport: StreamableHttpTransportConfig,
72
+ serverInfoName?: string,
73
+ ): ServerIdentity {
74
+ const url = new URL(typeof transport.url === 'string' ? transport.url : transport.url.toString());
75
+ const urlHostname = url.hostname;
76
+ const urlPath = url.pathname.length === 0 ? '/' : url.pathname;
77
+ // W-016: `url.host` (hostname:port for non-default ports) - two local
78
+ // servers on different ports must not collide into one identity.
79
+ const id = serverInfoName ?? `${url.host}${urlPath}`;
80
+ return Object.freeze({
81
+ kind: 'mcp-streamable-http',
82
+ id,
83
+ urlHostname,
84
+ urlPath,
85
+ ...(serverInfoName === undefined ? {} : { serverInfoName }),
86
+ });
87
+ }
88
+
89
+ function deriveSseIdentity(transport: SseTransportConfig, serverInfoName?: string): ServerIdentity {
90
+ const url = new URL(typeof transport.url === 'string' ? transport.url : transport.url.toString());
91
+ const urlHostname = url.hostname;
92
+ const urlPath = url.pathname.length === 0 ? '/' : url.pathname;
93
+ // W-016: see deriveStreamableHttpIdentity - port-inclusive host.
94
+ const id = serverInfoName ?? `${url.host}${urlPath}`;
95
+ return Object.freeze({
96
+ kind: 'mcp-sse',
97
+ id,
98
+ urlHostname,
99
+ urlPath,
100
+ ...(serverInfoName === undefined ? {} : { serverInfoName }),
101
+ });
102
+ }
103
+
104
+ function basenameWithoutExt(command: string): string {
105
+ const slashIdx = Math.max(command.lastIndexOf('/'), command.lastIndexOf('\\'));
106
+ const base = slashIdx === -1 ? command : command.slice(slashIdx + 1);
107
+ const dotIdx = base.lastIndexOf('.');
108
+ return dotIdx <= 0 ? base : base.slice(0, dotIdx);
109
+ }
110
+
111
+ /**
112
+ * Operator-facing single-line label for a {@link MCPTransportConfig}.
113
+ * Suitable for trace attributes, audit rows, and CLI output.
114
+ *
115
+ * @stable
116
+ */
117
+ export function formatMCPServerName(transport: MCPTransportConfig): string {
118
+ switch (transport.kind) {
119
+ case 'stdio':
120
+ return `stdio:${basenameWithoutExt(transport.command)}${
121
+ (transport.args ?? []).length === 0 ? '' : ` ${(transport.args ?? []).join(' ')}`
122
+ }`;
123
+ case 'streamable-http':
124
+ return `streamable-http:${typeof transport.url === 'string' ? transport.url : transport.url.toString()}`;
125
+ case 'sse':
126
+ return `sse:${typeof transport.url === 'string' ? transport.url : transport.url.toString()}`;
127
+ }
128
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Helper utilities exposed by `@graphorin/mcp`.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export { deriveServerIdentity, formatMCPServerName } from './identity.js';
8
+ export { validateMCPServerConfig } from './validate-config.js';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `validateMCPServerConfig(...)` - sanity-check a transport configuration
3
+ * before {@link createMCPClient} runs the network-level handshake.
4
+ *
5
+ * The validator catches the most common configuration mistakes
6
+ * (missing url for HTTP transports, empty command for stdio,
7
+ * unsupported `resumable: true` for stdio / sse, …) and surfaces them
8
+ * as {@link MCPInvalidConfigError} so the operator does not have to
9
+ * read a transport-specific stack trace.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import { MCPInvalidConfigError, MCPTransportNotSupportedError } from '../errors/index.js';
15
+ import type { MCPTransportConfig } from '../transport/types.js';
16
+
17
+ /**
18
+ * Throws {@link MCPInvalidConfigError} on invalid configuration.
19
+ *
20
+ * @stable
21
+ */
22
+ export function validateMCPServerConfig(opts: {
23
+ readonly transport: MCPTransportConfig;
24
+ readonly resumable?: boolean;
25
+ }): void {
26
+ validateTransport(opts.transport);
27
+ if (opts.resumable === true) {
28
+ if (opts.transport.kind === 'stdio') {
29
+ throw new MCPTransportNotSupportedError(
30
+ 'Resumable streaming sessions are not supported on the stdio transport.',
31
+ {
32
+ variant: 'transport-resumable-not-supported',
33
+ metadata: { transport: 'stdio' },
34
+ hint: 'Use the streamable-http transport when you need Mcp-Session-Id + Last-Event-ID resume semantics.',
35
+ },
36
+ );
37
+ }
38
+ if (opts.transport.kind === 'sse') {
39
+ throw new MCPTransportNotSupportedError(
40
+ 'Resumable streaming sessions are not supported on the deprecated sse transport.',
41
+ {
42
+ variant: 'transport-resumable-not-supported',
43
+ metadata: { transport: 'sse' },
44
+ hint: 'Migrate to the streamable-http transport for resumable session support.',
45
+ },
46
+ );
47
+ }
48
+ }
49
+ }
50
+
51
+ function validateTransport(transport: MCPTransportConfig): void {
52
+ switch (transport.kind) {
53
+ case 'stdio': {
54
+ if (typeof transport.command !== 'string' || transport.command.length === 0) {
55
+ throw new MCPInvalidConfigError('stdio transport requires a non-empty `command`.', {
56
+ metadata: { transport: 'stdio' },
57
+ });
58
+ }
59
+ if (transport.args !== undefined && !Array.isArray(transport.args)) {
60
+ throw new MCPInvalidConfigError('stdio transport `args` must be an array of strings.', {
61
+ metadata: { transport: 'stdio' },
62
+ });
63
+ }
64
+ return;
65
+ }
66
+ case 'streamable-http':
67
+ case 'sse': {
68
+ if (transport.url === undefined || transport.url === null || transport.url === '') {
69
+ throw new MCPInvalidConfigError(
70
+ `${transport.kind} transport requires a non-empty \`url\`.`,
71
+ {
72
+ metadata: { transport: transport.kind },
73
+ },
74
+ );
75
+ }
76
+ try {
77
+ const candidate =
78
+ typeof transport.url === 'string' ? new URL(transport.url) : transport.url;
79
+ if (candidate.protocol !== 'http:' && candidate.protocol !== 'https:') {
80
+ throw new MCPInvalidConfigError(
81
+ `${transport.kind} transport \`url\` must use http: or https: (got ${candidate.protocol}).`,
82
+ { metadata: { transport: transport.kind } },
83
+ );
84
+ }
85
+ } catch (cause) {
86
+ if (cause instanceof MCPInvalidConfigError) throw cause;
87
+ throw new MCPInvalidConfigError(`${transport.kind} transport \`url\` is not a valid URL.`, {
88
+ metadata: { transport: transport.kind },
89
+ cause,
90
+ });
91
+ }
92
+ return;
93
+ }
94
+ }
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `@graphorin/mcp` - Model Context Protocol client for the Graphorin
3
+ * framework.
4
+ *
5
+ * The package owns:
6
+ *
7
+ * - The {@link createMCPClient} factory that opens a typed MCP
8
+ * connection over stdio, Streamable HTTP, or the deprecated SSE
9
+ * transport.
10
+ * - The {@link MCPClient} surface (`listTools` / `listResources` /
11
+ * `listPrompts` / `callTool` / `readResource` / `getPrompt` /
12
+ * `close`) plus the strategy-aware {@link MCPClient.toTools}
13
+ * adapter that bridges MCP tool descriptors into Graphorin
14
+ * `Tool` records.
15
+ * - The OAuth bridge that resolves bearer headers from the
16
+ * {@link OAuthAuthorizationProvider} backed by
17
+ * `@graphorin/security/oauth`.
18
+ * - Library helpers (`mcpAuthLogin`, `mcpAuthListSessions`,
19
+ * `mcpAuthRefresh`, `mcpAuthRevoke`, `mcpAuthStatus`) consumed by
20
+ * the upcoming `graphorin auth` CLI surface.
21
+ * - Typed errors ({@link MCPConnectionError},
22
+ * {@link MCPProtocolError}, {@link MCPAuthError},
23
+ * {@link MCPToolNotFoundError}, {@link MCPCallTimeoutError},
24
+ * {@link MCPCancelledError}, {@link MCPInvalidConfigError},
25
+ * {@link MCPTransportNotSupportedError}, {@link GraphorinMCPError}).
26
+ *
27
+ * Stable sub-paths:
28
+ *
29
+ * ```ts
30
+ * import { createMCPClient } from '@graphorin/mcp/client';
31
+ * import { createOAuthAuthorizationProvider } from '@graphorin/mcp/oauth';
32
+ * import { formatMCPServerName, validateMCPServerConfig } from '@graphorin/mcp/helpers';
33
+ * import { MCPConnectionError } from '@graphorin/mcp/errors';
34
+ * import type { MCPTransportConfig, ServerIdentity } from '@graphorin/mcp/transport';
35
+ * ```
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+
40
+ /** Canonical version constant, derived from `package.json` at build time. */
41
+ import pkg from '../package.json' with { type: 'json' };
42
+
43
+ export const VERSION: string = pkg.version;
44
+
45
+ export * from './client/index.js';
46
+ export * from './errors/index.js';
47
+ export * from './helpers/index.js';
48
+ export * from './oauth/index.js';
49
+ export * from './transport/index.js';
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Bridge between the MCP transport's bearer-token requirement and the
3
+ * outbound OAuth subsystem in `@graphorin/security/oauth`.
4
+ *
5
+ * The bridge resolves an authorization header from a stored
6
+ * {@link OAuthSession}, refreshes the token automatically when it is
7
+ * within the configured pre-expiry window, and surfaces every
8
+ * lifecycle transition (`oauth.granted` / `oauth.refreshed` /
9
+ * `oauth.revoked` / `mcp.auth.expired`) to subscribers.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import type { OAuthServerStore } from '@graphorin/core/contracts';
15
+ import type { OAuthSession } from '@graphorin/security/oauth';
16
+ import {
17
+ emitOAuthLifecycle,
18
+ GraphorinOAuthError,
19
+ refreshOAuthSession,
20
+ } from '@graphorin/security/oauth';
21
+
22
+ import { MCPAuthError } from '../errors/index.js';
23
+
24
+ /** Options accepted by {@link createOAuthAuthorizationProvider}. */
25
+ export interface OAuthAuthorizationProviderOptions {
26
+ /**
27
+ * Secrets store the persisted tokens resolve from (SPL-1) - with it,
28
+ * the bridge issues Authorization headers across process restarts.
29
+ */
30
+ readonly secretsStore?: import('@graphorin/core/contracts').SecretsStore;
31
+ /** Stable identifier of the persisted OAuth server (`serverId`). */
32
+ readonly serverId: string;
33
+ /** Persistent storage. */
34
+ readonly storage: OAuthServerStore;
35
+ /**
36
+ * Time-to-refresh window in milliseconds. When the session is
37
+ * within `refreshAheadMs` of expiry the provider triggers a
38
+ * refresh on the next request. Defaults to 5 minutes.
39
+ */
40
+ readonly refreshAheadMs?: number;
41
+ /** Optional per-request `AbortSignal` (forwarded to refresh). */
42
+ readonly signal?: AbortSignal;
43
+ }
44
+
45
+ /**
46
+ * Live authorization-header provider returned by
47
+ * {@link createOAuthAuthorizationProvider}.
48
+ *
49
+ * @stable
50
+ */
51
+ export interface OAuthAuthorizationProvider {
52
+ /** Resolve an `Authorization: Bearer ...` header. */
53
+ resolveHeader(): Promise<string>;
54
+ /** Force a refresh, regardless of expiry. */
55
+ refresh(): Promise<OAuthSession>;
56
+ /** Persist the most recently observed expiry timestamp. */
57
+ readonly serverId: string;
58
+ }
59
+
60
+ /**
61
+ * Build a provider that resolves the `Authorization` header value the
62
+ * Streamable HTTP / SSE MCP transports send on every request.
63
+ *
64
+ * The provider:
65
+ *
66
+ * 1. Loads the persisted session metadata from the supplied store.
67
+ * 2. Refreshes the session when it is within `refreshAheadMs` of
68
+ * expiry.
69
+ * 3. Wraps every refresh failure in {@link MCPAuthError} carrying a
70
+ * `hint` that points the operator to the upcoming
71
+ * `graphorin auth refresh` CLI.
72
+ *
73
+ * @stable
74
+ */
75
+ export function createOAuthAuthorizationProvider(
76
+ options: OAuthAuthorizationProviderOptions,
77
+ ): OAuthAuthorizationProvider {
78
+ const refreshAheadMs = options.refreshAheadMs ?? 5 * 60_000;
79
+ let activeSession: OAuthSession | undefined;
80
+
81
+ async function loadOrRefresh(force: boolean): Promise<OAuthSession> {
82
+ const record = await options.storage.get(options.serverId);
83
+ if (record === null) {
84
+ throw new MCPAuthError(`OAuth server '${options.serverId}' is not registered.`, {
85
+ hint: `run 'graphorin auth login --mcp ${options.serverId} --url <server-url>' to register the OAuth server.`,
86
+ metadata: { server: options.serverId },
87
+ });
88
+ }
89
+ const expiresAt = record.expiresAt;
90
+ const now = Date.now();
91
+ const stale =
92
+ force ||
93
+ activeSession === undefined ||
94
+ (expiresAt !== undefined && expiresAt - now < refreshAheadMs);
95
+ if (!stale && activeSession !== undefined) return activeSession;
96
+ try {
97
+ const session = await refreshOAuthSession(options.storage, options.serverId, {
98
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
99
+ ...(options.secretsStore === undefined ? {} : { secretsStore: options.secretsStore }),
100
+ });
101
+ activeSession = session;
102
+ return session;
103
+ } catch (cause) {
104
+ if (cause instanceof GraphorinOAuthError) {
105
+ emitOAuthLifecycle({
106
+ type: 'mcp.auth.expired',
107
+ serverId: options.serverId,
108
+ ts: Date.now(),
109
+ reason: cause.kind,
110
+ metadata: { serverUrl: record.serverUrl },
111
+ });
112
+ throw new MCPAuthError(
113
+ `MCP OAuth session for '${options.serverId}' could not be refreshed: ${cause.message}`,
114
+ {
115
+ hint: `run 'graphorin auth refresh --mcp ${options.serverId}' to recover.`,
116
+ metadata: { server: options.serverId },
117
+ cause,
118
+ },
119
+ );
120
+ }
121
+ throw new MCPAuthError(
122
+ `MCP OAuth session for '${options.serverId}' could not be refreshed.`,
123
+ {
124
+ hint: `run 'graphorin auth refresh --mcp ${options.serverId}' to recover.`,
125
+ metadata: { server: options.serverId },
126
+ cause,
127
+ },
128
+ );
129
+ }
130
+ }
131
+
132
+ return Object.freeze({
133
+ serverId: options.serverId,
134
+ async resolveHeader(): Promise<string> {
135
+ const session = await loadOrRefresh(false);
136
+ const tokenType = session.tokenType.length === 0 ? 'Bearer' : session.tokenType;
137
+ return session.accessToken.use((raw) => `${tokenType} ${raw}`);
138
+ },
139
+ async refresh(): Promise<OAuthSession> {
140
+ return loadOrRefresh(true);
141
+ },
142
+ });
143
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * OAuth integration surface for `@graphorin/mcp`.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export {
8
+ createOAuthAuthorizationProvider,
9
+ type OAuthAuthorizationProvider,
10
+ type OAuthAuthorizationProviderOptions,
11
+ } from './bridge.js';
12
+ export {
13
+ mcpAuthListSessions,
14
+ mcpAuthLogin,
15
+ mcpAuthRefresh,
16
+ mcpAuthRevoke,
17
+ mcpAuthStatus,
18
+ } from './library.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Thin re-export wrappers for the OAuth library functions consumed by
3
+ * the upcoming `graphorin auth login | list | refresh | revoke |
4
+ * status` CLI surface (Phase 15). The wrappers attach an
5
+ * `mcpServerId` audit tag so downstream observers can attribute
6
+ * events to the MCP subsystem rather than to a generic OAuth call
7
+ * site.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import type { OAuthServerStore } from '@graphorin/core/contracts';
13
+ import {
14
+ getOAuthStatus,
15
+ type LoginInteractiveOptions,
16
+ type LoginInteractiveResult,
17
+ listOAuthSessions,
18
+ loginInteractive,
19
+ type OAuthSession,
20
+ type OAuthSessionMetadata,
21
+ type OAuthStatusSnapshot,
22
+ refreshOAuthSession,
23
+ revokeOAuthSession,
24
+ } from '@graphorin/security/oauth';
25
+
26
+ /** Drive `graphorin auth login --mcp <id>`. */
27
+ export async function mcpAuthLogin(
28
+ options: LoginInteractiveOptions,
29
+ ): Promise<LoginInteractiveResult> {
30
+ return loginInteractive(options);
31
+ }
32
+
33
+ /** Drive `graphorin auth list --mcp`. */
34
+ export async function mcpAuthListSessions(
35
+ storage: OAuthServerStore,
36
+ ): Promise<ReadonlyArray<OAuthSessionMetadata>> {
37
+ return listOAuthSessions(storage);
38
+ }
39
+
40
+ /** Drive `graphorin auth refresh --mcp <id>`. */
41
+ export async function mcpAuthRefresh(
42
+ storage: OAuthServerStore,
43
+ serverId: string,
44
+ options: { readonly signal?: AbortSignal } = {},
45
+ ): Promise<OAuthSession> {
46
+ return refreshOAuthSession(storage, serverId, options);
47
+ }
48
+
49
+ /** Drive `graphorin auth revoke --mcp <id>`. */
50
+ export async function mcpAuthRevoke(
51
+ storage: OAuthServerStore,
52
+ serverId: string,
53
+ options: { readonly reason?: string; readonly signal?: AbortSignal } = {},
54
+ ): Promise<void> {
55
+ await revokeOAuthSession(storage, serverId, options);
56
+ }
57
+
58
+ /** Drive `graphorin auth status --mcp`. */
59
+ export async function mcpAuthStatus(storage: OAuthServerStore): Promise<OAuthStatusSnapshot> {
60
+ return getOAuthStatus(storage);
61
+ }