@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.
- package/CHANGELOG.md +43 -0
- package/README.md +64 -3
- package/dist/client/adapt-result.d.ts +9 -1
- package/dist/client/adapt-result.d.ts.map +1 -1
- package/dist/client/adapt-result.js +28 -10
- package/dist/client/adapt-result.js.map +1 -1
- package/dist/client/client-handlers.js +7 -1
- package/dist/client/client-handlers.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +45 -70
- package/dist/client/client.js.map +1 -1
- package/dist/client/inbound-filters.js +101 -1
- package/dist/client/inbound-filters.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/client/managed.d.ts +35 -0
- package/dist/client/managed.d.ts.map +1 -0
- package/dist/client/managed.js +136 -0
- package/dist/client/managed.js.map +1 -0
- package/dist/client/mcp-resource-reader.js +1 -1
- package/dist/client/mcp-resource-reader.js.map +1 -1
- package/dist/client/to-tools-run.js +119 -0
- package/dist/client/to-tools-run.js.map +1 -0
- package/dist/client/to-tools.d.ts +8 -0
- package/dist/client/to-tools.d.ts.map +1 -1
- package/dist/client/to-tools.js +27 -4
- package/dist/client/to-tools.js.map +1 -1
- package/dist/client/types.d.ts +12 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors/index.d.ts +3 -3
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/helpers/identity.d.ts +11 -0
- package/dist/helpers/identity.d.ts.map +1 -1
- package/dist/helpers/identity.js +13 -2
- package/dist/helpers/identity.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/json-schema.js +26 -8
- package/dist/registry/json-schema.js.map +1 -1
- package/dist/transport/types.d.ts +9 -0
- package/package.json +13 -12
- package/src/client/adapt-result.ts +302 -0
- package/src/client/client-handlers.ts +215 -0
- package/src/client/client.ts +725 -0
- package/src/client/defer-loading.ts +108 -0
- package/src/client/inbound-filters.ts +246 -0
- package/src/client/index.ts +42 -0
- package/src/client/managed.ts +222 -0
- package/src/client/mcp-resource-reader.ts +183 -0
- package/src/client/pinning.ts +48 -0
- package/src/client/to-tools-run.ts +178 -0
- package/src/client/to-tools.ts +294 -0
- package/src/client/transport-factory.ts +117 -0
- package/src/client/types.ts +422 -0
- package/src/errors/index.ts +170 -0
- package/src/helpers/identity.ts +128 -0
- package/src/helpers/index.ts +8 -0
- package/src/helpers/validate-config.ts +95 -0
- package/src/index.ts +49 -0
- package/src/oauth/bridge.ts +143 -0
- package/src/oauth/index.ts +18 -0
- package/src/oauth/library.ts +61 -0
- package/src/registry/json-schema.ts +402 -0
- package/src/transport/index.ts +15 -0
- 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,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
|
+
}
|