@olane/o-mcp 0.6.13 → 0.7.2
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/src/mcp-bridge.tool.d.ts +7 -4
- package/dist/src/mcp-bridge.tool.d.ts.map +1 -1
- package/dist/src/mcp-bridge.tool.js +17 -12
- package/dist/src/mcp.tool.d.ts +12 -5
- package/dist/src/mcp.tool.d.ts.map +1 -1
- package/dist/src/mcp.tool.js +8 -10
- package/dist/src/methods/mcp-bridge.methods.d.ts.map +1 -1
- package/dist/src/methods/mcp-bridge.methods.js +16 -2
- package/dist/src/o-client.mcp.d.ts.map +1 -1
- package/dist/src/oauth/index.d.ts +8 -0
- package/dist/src/oauth/index.d.ts.map +1 -0
- package/dist/src/oauth/index.js +7 -0
- package/dist/src/oauth/interfaces/client-metadata.interface.d.ts +17 -0
- package/dist/src/oauth/interfaces/client-metadata.interface.d.ts.map +1 -0
- package/dist/src/oauth/interfaces/client-metadata.interface.js +1 -0
- package/dist/src/oauth/interfaces/index.d.ts +4 -0
- package/dist/src/oauth/interfaces/index.d.ts.map +1 -0
- package/dist/src/oauth/interfaces/index.js +3 -0
- package/dist/src/oauth/interfaces/oauth-client-info.interface.d.ts +12 -0
- package/dist/src/oauth/interfaces/oauth-client-info.interface.d.ts.map +1 -0
- package/dist/src/oauth/interfaces/oauth-client-info.interface.js +1 -0
- package/dist/src/oauth/interfaces/oauth-tokens.interface.d.ts +10 -0
- package/dist/src/oauth/interfaces/oauth-tokens.interface.d.ts.map +1 -0
- package/dist/src/oauth/interfaces/oauth-tokens.interface.js +1 -0
- package/dist/src/oauth/mcp-dynamic-registration.d.ts +33 -0
- package/dist/src/oauth/mcp-dynamic-registration.d.ts.map +1 -0
- package/dist/src/oauth/mcp-dynamic-registration.js +107 -0
- package/dist/src/oauth/mcp-oauth-callback-server.d.ts +51 -0
- package/dist/src/oauth/mcp-oauth-callback-server.d.ts.map +1 -0
- package/dist/src/oauth/mcp-oauth-callback-server.js +179 -0
- package/dist/src/oauth/mcp-oauth-manager.d.ts +61 -0
- package/dist/src/oauth/mcp-oauth-manager.d.ts.map +1 -0
- package/dist/src/oauth/mcp-oauth-manager.js +289 -0
- package/dist/src/oauth/mcp-oauth-storage.d.ts +60 -0
- package/dist/src/oauth/mcp-oauth-storage.d.ts.map +1 -0
- package/dist/src/oauth/mcp-oauth-storage.js +146 -0
- package/dist/src/oauth/methods/mcp-oauth.methods.d.ts +5 -0
- package/dist/src/oauth/methods/mcp-oauth.methods.d.ts.map +1 -0
- package/dist/src/oauth/methods/mcp-oauth.methods.js +99 -0
- package/dist/src/oauth/oauth-aware-transport.d.ts +50 -0
- package/dist/src/oauth/oauth-aware-transport.d.ts.map +1 -0
- package/dist/src/oauth/oauth-aware-transport.js +120 -0
- package/dist/test/benchmark.spec.d.ts +0 -1
- package/dist/test/benchmark.spec.js +49 -48
- package/dist/test/tools.spec.d.ts +2 -0
- package/dist/test/tools.spec.d.ts.map +1 -0
- package/dist/test/tools.spec.js +13 -0
- package/package.json +20 -14
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
/**
|
|
4
|
+
* HTTP server for handling OAuth callbacks
|
|
5
|
+
*
|
|
6
|
+
* This server runs locally to receive the OAuth redirect after user authorization.
|
|
7
|
+
* It automatically shuts down after receiving the callback.
|
|
8
|
+
*/
|
|
9
|
+
export class McpOAuthCallbackServer {
|
|
10
|
+
constructor(port) {
|
|
11
|
+
this.server = null;
|
|
12
|
+
this.port = 3334;
|
|
13
|
+
this.callbackResult = null;
|
|
14
|
+
if (port) {
|
|
15
|
+
this.port = port;
|
|
16
|
+
}
|
|
17
|
+
this.app = express();
|
|
18
|
+
this.events = new EventEmitter();
|
|
19
|
+
this.setupRoutes();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Setup Express routes
|
|
23
|
+
*/
|
|
24
|
+
setupRoutes() {
|
|
25
|
+
// OAuth callback endpoint
|
|
26
|
+
this.app.get('/oauth/callback', (req, res) => {
|
|
27
|
+
const { code, state, error, error_description } = req.query;
|
|
28
|
+
if (error) {
|
|
29
|
+
this.callbackResult = {
|
|
30
|
+
code: '',
|
|
31
|
+
error: error,
|
|
32
|
+
error_description: error_description,
|
|
33
|
+
};
|
|
34
|
+
res.send(`
|
|
35
|
+
<html>
|
|
36
|
+
<head><title>OAuth Error</title></head>
|
|
37
|
+
<body style="font-family: Arial, sans-serif; padding: 40px; text-align: center;">
|
|
38
|
+
<h1 style="color: #d32f2f;">Authentication Failed</h1>
|
|
39
|
+
<p style="color: #666;">Error: ${error}</p>
|
|
40
|
+
<p style="color: #666;">${error_description || ''}</p>
|
|
41
|
+
<p style="margin-top: 40px; color: #999;">You can close this window.</p>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
else if (code) {
|
|
47
|
+
this.callbackResult = {
|
|
48
|
+
code: code,
|
|
49
|
+
state: state,
|
|
50
|
+
};
|
|
51
|
+
res.send(`
|
|
52
|
+
<html>
|
|
53
|
+
<head><title>OAuth Success</title></head>
|
|
54
|
+
<body style="font-family: Arial, sans-serif; padding: 40px; text-align: center;">
|
|
55
|
+
<h1 style="color: #4caf50;">✓ Authentication Successful</h1>
|
|
56
|
+
<p style="color: #666;">You have successfully authenticated with the MCP server.</p>
|
|
57
|
+
<p style="margin-top: 40px; color: #999;">You can close this window.</p>
|
|
58
|
+
<script>
|
|
59
|
+
setTimeout(() => window.close(), 3000);
|
|
60
|
+
</script>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.callbackResult = {
|
|
67
|
+
code: '',
|
|
68
|
+
error: 'invalid_request',
|
|
69
|
+
error_description: 'No code or error in callback',
|
|
70
|
+
};
|
|
71
|
+
res.status(400).send(`
|
|
72
|
+
<html>
|
|
73
|
+
<head><title>OAuth Error</title></head>
|
|
74
|
+
<body style="font-family: Arial, sans-serif; padding: 40px; text-align: center;">
|
|
75
|
+
<h1 style="color: #d32f2f;">Invalid Callback</h1>
|
|
76
|
+
<p style="color: #666;">The OAuth callback was missing required parameters.</p>
|
|
77
|
+
<p style="margin-top: 40px; color: #999;">You can close this window.</p>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
// Emit callback event
|
|
83
|
+
this.events.emit('callback', this.callbackResult);
|
|
84
|
+
});
|
|
85
|
+
// Health check endpoint
|
|
86
|
+
this.app.get('/health', (req, res) => {
|
|
87
|
+
res.json({ status: 'ok', port: this.port });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Start the callback server
|
|
92
|
+
*/
|
|
93
|
+
async start(port) {
|
|
94
|
+
if (port) {
|
|
95
|
+
this.port = port;
|
|
96
|
+
}
|
|
97
|
+
if (this.server) {
|
|
98
|
+
throw new Error('Callback server is already running');
|
|
99
|
+
}
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
try {
|
|
102
|
+
this.server = this.app.listen(this.port, () => {
|
|
103
|
+
console.log(`OAuth callback server listening on http://localhost:${this.port}`);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
this.server.on('error', (error) => {
|
|
107
|
+
if (error.code === 'EADDRINUSE') {
|
|
108
|
+
reject(new Error(`Port ${this.port} is already in use. Please close other OAuth flows or choose a different port.`));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reject(error);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
reject(error);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Stop the callback server
|
|
122
|
+
*/
|
|
123
|
+
async stop() {
|
|
124
|
+
if (!this.server) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
this.server.close((error) => {
|
|
129
|
+
if (error) {
|
|
130
|
+
reject(error);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.server = null;
|
|
134
|
+
this.callbackResult = null;
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wait for OAuth callback
|
|
142
|
+
* @param timeout Timeout in milliseconds (default: 5 minutes)
|
|
143
|
+
*/
|
|
144
|
+
async waitForCallback(timeout = 300000) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const timeoutId = setTimeout(() => {
|
|
147
|
+
this.events.removeAllListeners('callback');
|
|
148
|
+
reject(new Error('OAuth callback timeout. User did not complete authentication.'));
|
|
149
|
+
}, timeout);
|
|
150
|
+
this.events.once('callback', (result) => {
|
|
151
|
+
clearTimeout(timeoutId);
|
|
152
|
+
if (result.error) {
|
|
153
|
+
reject(new Error(`OAuth error: ${result.error}. ${result.error_description || ''}`));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
resolve(result);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the callback URL
|
|
163
|
+
*/
|
|
164
|
+
getCallbackUrl() {
|
|
165
|
+
return `http://localhost:${this.port}/oauth/callback`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get the current port
|
|
169
|
+
*/
|
|
170
|
+
getPort() {
|
|
171
|
+
return this.port;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if server is running
|
|
175
|
+
*/
|
|
176
|
+
isRunning() {
|
|
177
|
+
return this.server !== null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { oRequest } from '@olane/o-core';
|
|
2
|
+
import { oLaneTool } from '@olane/o-lane';
|
|
3
|
+
import { oNodeToolConfig } from '@olane/o-node';
|
|
4
|
+
import { ToolResult } from '@olane/o-tool';
|
|
5
|
+
/**
|
|
6
|
+
* MCP OAuth Manager Tool
|
|
7
|
+
*
|
|
8
|
+
* Coordinates OAuth authentication flows for MCP servers:
|
|
9
|
+
* - Dynamic client registration (RFC 7591)
|
|
10
|
+
* - PKCE authorization flow
|
|
11
|
+
* - Token storage and refresh
|
|
12
|
+
* - Callback server management
|
|
13
|
+
*/
|
|
14
|
+
export declare class McpOAuthManager extends oLaneTool {
|
|
15
|
+
private storage;
|
|
16
|
+
private callbackServer;
|
|
17
|
+
private dynamicRegistration;
|
|
18
|
+
private oauthToolAddress;
|
|
19
|
+
constructor(config: oNodeToolConfig);
|
|
20
|
+
/**
|
|
21
|
+
* Authenticate with an OAuth-protected MCP server
|
|
22
|
+
*/
|
|
23
|
+
_tool_authenticate_server(request: oRequest): Promise<ToolResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Refresh OAuth tokens for an MCP server
|
|
26
|
+
*/
|
|
27
|
+
_tool_refresh_tokens(request: oRequest): Promise<ToolResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Check OAuth token status for an MCP server
|
|
30
|
+
*/
|
|
31
|
+
_tool_check_tokens(request: oRequest): Promise<ToolResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Clear OAuth tokens and client info for an MCP server
|
|
34
|
+
*/
|
|
35
|
+
_tool_clear_tokens(request: oRequest): Promise<ToolResult>;
|
|
36
|
+
/**
|
|
37
|
+
* List all authenticated MCP servers
|
|
38
|
+
*/
|
|
39
|
+
_tool_list_authenticated_servers(request: oRequest): Promise<ToolResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Public method for refreshing tokens (used by transport)
|
|
42
|
+
*/
|
|
43
|
+
refreshTokens(serverUrl: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Get service name for OAuth tool
|
|
46
|
+
*/
|
|
47
|
+
private getServiceName;
|
|
48
|
+
/**
|
|
49
|
+
* Hash server URL for service name
|
|
50
|
+
*/
|
|
51
|
+
private hashServerUrl;
|
|
52
|
+
/**
|
|
53
|
+
* Launch browser for OAuth authorization
|
|
54
|
+
*/
|
|
55
|
+
private launchBrowser;
|
|
56
|
+
/**
|
|
57
|
+
* Cleanup on shutdown
|
|
58
|
+
*/
|
|
59
|
+
stop(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=mcp-oauth-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-oauth-manager.d.ts","sourceRoot":"","sources":["../../../src/oauth/mcp-oauth-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAO3C;;;;;;;;GAQG;AACH,qBAAa,eAAgB,SAAQ,SAAS;IAC5C,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,mBAAmB,CAAyB;IACpD,OAAO,CAAC,gBAAgB,CAAW;gBAEvB,MAAM,EAAE,eAAe;IAkBnC;;OAEG;IACG,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAyLvE;;OAEG;IACG,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAwClE;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAehE;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAahE;;OAEG;IACG,gCAAgC,CACpC,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;IAWtB;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;OAEG;YACW,aAAa;IAY3B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { oAddress } from '@olane/o-core';
|
|
2
|
+
import { oLaneTool } from '@olane/o-lane';
|
|
3
|
+
import { McpOAuthStorage } from './mcp-oauth-storage.js';
|
|
4
|
+
import { McpOAuthCallbackServer } from './mcp-oauth-callback-server.js';
|
|
5
|
+
import { McpDynamicRegistration } from './mcp-dynamic-registration.js';
|
|
6
|
+
import { MCP_OAUTH_METHODS } from './methods/mcp-oauth.methods.js';
|
|
7
|
+
/**
|
|
8
|
+
* MCP OAuth Manager Tool
|
|
9
|
+
*
|
|
10
|
+
* Coordinates OAuth authentication flows for MCP servers:
|
|
11
|
+
* - Dynamic client registration (RFC 7591)
|
|
12
|
+
* - PKCE authorization flow
|
|
13
|
+
* - Token storage and refresh
|
|
14
|
+
* - Callback server management
|
|
15
|
+
*/
|
|
16
|
+
export class McpOAuthManager extends oLaneTool {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
super({
|
|
19
|
+
...config,
|
|
20
|
+
address: new oAddress('o://mcp/oauth'),
|
|
21
|
+
description: 'OAuth authentication manager for MCP servers',
|
|
22
|
+
methods: MCP_OAUTH_METHODS,
|
|
23
|
+
});
|
|
24
|
+
this.callbackServer = null;
|
|
25
|
+
// Initialize storage adapter with o://secure
|
|
26
|
+
this.storage = new McpOAuthStorage({
|
|
27
|
+
storageAddress: new oAddress('o://secure'),
|
|
28
|
+
useFunction: this.use.bind(this),
|
|
29
|
+
});
|
|
30
|
+
this.dynamicRegistration = new McpDynamicRegistration();
|
|
31
|
+
this.oauthToolAddress = new oAddress('o://oauth');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Authenticate with an OAuth-protected MCP server
|
|
35
|
+
*/
|
|
36
|
+
async _tool_authenticate_server(request) {
|
|
37
|
+
const { serverUrl, clientName, scope, staticClientInfo, useDynamicRegistration = true, callbackPort = 3334, } = request.params;
|
|
38
|
+
try {
|
|
39
|
+
this.logger.info(`Starting OAuth authentication for ${serverUrl}`);
|
|
40
|
+
// 1. Check if we already have valid tokens
|
|
41
|
+
const existingTokens = await this.storage.getTokens(serverUrl);
|
|
42
|
+
if (existingTokens &&
|
|
43
|
+
!(await this.storage.areTokensExpired(serverUrl))) {
|
|
44
|
+
this.logger.info(`Already authenticated with ${serverUrl}`);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
message: 'Already authenticated',
|
|
48
|
+
hasTokens: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// 2. Get or register OAuth client
|
|
52
|
+
let clientInfo;
|
|
53
|
+
if (staticClientInfo) {
|
|
54
|
+
// Use provided static client credentials
|
|
55
|
+
this.logger.info(`Using static OAuth client info for ${serverUrl}`);
|
|
56
|
+
const staticInfo = staticClientInfo;
|
|
57
|
+
// Discover endpoints if not provided
|
|
58
|
+
const endpoints = await this.dynamicRegistration.discoverEndpoints(serverUrl);
|
|
59
|
+
clientInfo = {
|
|
60
|
+
client_id: staticInfo.client_id,
|
|
61
|
+
client_secret: staticInfo.client_secret,
|
|
62
|
+
authorization_endpoint: staticInfo.authorization_endpoint ||
|
|
63
|
+
endpoints.authorization_endpoint ||
|
|
64
|
+
'',
|
|
65
|
+
token_endpoint: staticInfo.token_endpoint || endpoints.token_endpoint || '',
|
|
66
|
+
scope: (staticInfo.scope ||
|
|
67
|
+
scope ||
|
|
68
|
+
'openid profile email'),
|
|
69
|
+
};
|
|
70
|
+
if (!clientInfo.authorization_endpoint || !clientInfo.token_endpoint) {
|
|
71
|
+
throw new Error(`Could not determine OAuth endpoints for ${serverUrl}. Please provide authorization_endpoint and token_endpoint in staticClientInfo.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (useDynamicRegistration) {
|
|
75
|
+
// Dynamic client registration
|
|
76
|
+
this.logger.info(`Attempting dynamic client registration for ${serverUrl}`);
|
|
77
|
+
const existingClientInfo = await this.storage.getClientInfo(serverUrl);
|
|
78
|
+
// Start callback server to get redirect URI
|
|
79
|
+
this.callbackServer = new McpOAuthCallbackServer(callbackPort);
|
|
80
|
+
await this.callbackServer.start();
|
|
81
|
+
const redirectUri = this.callbackServer.getCallbackUrl();
|
|
82
|
+
clientInfo = await this.dynamicRegistration.getOrRegisterClient(serverUrl, clientName || 'Olane MCP Client', redirectUri, existingClientInfo, scope);
|
|
83
|
+
// Save client info to encrypted storage
|
|
84
|
+
await this.storage.saveClientInfo(serverUrl, clientInfo);
|
|
85
|
+
this.logger.info(`Client registered successfully for ${serverUrl}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
throw new Error(`No OAuth client credentials provided for ${serverUrl}. Please provide staticClientInfo or enable useDynamicRegistration.`);
|
|
89
|
+
}
|
|
90
|
+
// 3. Start callback server if not already started
|
|
91
|
+
if (!this.callbackServer) {
|
|
92
|
+
this.callbackServer = new McpOAuthCallbackServer(callbackPort);
|
|
93
|
+
await this.callbackServer.start();
|
|
94
|
+
}
|
|
95
|
+
const redirectUri = this.callbackServer.getCallbackUrl();
|
|
96
|
+
// 4. Configure OAuth via existing OAuthTool
|
|
97
|
+
const serviceName = this.getServiceName(serverUrl);
|
|
98
|
+
await this.use(this.oauthToolAddress, {
|
|
99
|
+
method: 'configure',
|
|
100
|
+
params: {
|
|
101
|
+
serviceName,
|
|
102
|
+
clientId: clientInfo.client_id,
|
|
103
|
+
clientSecret: clientInfo.client_secret,
|
|
104
|
+
redirectUri,
|
|
105
|
+
authorizationUrl: clientInfo.authorization_endpoint,
|
|
106
|
+
tokenUrl: clientInfo.token_endpoint,
|
|
107
|
+
scope: clientInfo.scope || scope || 'openid profile email',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
// 5. Get authorization URL (with PKCE)
|
|
111
|
+
const authUrlResponse = await this.use(this.oauthToolAddress, {
|
|
112
|
+
method: 'getAuthorizationUrl',
|
|
113
|
+
params: {
|
|
114
|
+
serviceName,
|
|
115
|
+
scope: clientInfo.scope || scope,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const authUrl = authUrlResponse.result?.data?.authorizationUrl ||
|
|
119
|
+
authUrlResponse.result?.authorizationUrl;
|
|
120
|
+
this.logger.info(`Authorization URL: ${authUrl}`);
|
|
121
|
+
// 6. Launch browser
|
|
122
|
+
this.logger.info('Opening browser for user authorization...');
|
|
123
|
+
await this.launchBrowser(authUrl);
|
|
124
|
+
// 7. Wait for callback
|
|
125
|
+
this.logger.info('Waiting for OAuth callback...');
|
|
126
|
+
const { code, state } = await this.callbackServer.waitForCallback(300000); // 5 min timeout
|
|
127
|
+
this.logger.info('OAuth callback received');
|
|
128
|
+
// 8. Exchange code for tokens
|
|
129
|
+
const tokenResponse = await this.use(this.oauthToolAddress, {
|
|
130
|
+
method: 'exchangeCode',
|
|
131
|
+
params: {
|
|
132
|
+
serviceName,
|
|
133
|
+
code,
|
|
134
|
+
state,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const tokens = tokenResponse.result?.data?.tokens ||
|
|
138
|
+
tokenResponse.result?.tokens;
|
|
139
|
+
// 9. Persist tokens to encrypted storage
|
|
140
|
+
await this.storage.saveTokens(serverUrl, tokens);
|
|
141
|
+
// 10. Stop callback server
|
|
142
|
+
await this.callbackServer.stop();
|
|
143
|
+
this.callbackServer = null;
|
|
144
|
+
this.logger.info(`OAuth authentication successful for ${serverUrl}`);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
message: 'OAuth authentication successful',
|
|
148
|
+
serverUrl,
|
|
149
|
+
hasTokens: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// Clean up callback server on error
|
|
154
|
+
if (this.callbackServer) {
|
|
155
|
+
await this.callbackServer.stop().catch(() => { });
|
|
156
|
+
this.callbackServer = null;
|
|
157
|
+
}
|
|
158
|
+
this.logger.error(`OAuth authentication failed: ${error.message}`);
|
|
159
|
+
throw new Error(`OAuth authentication failed: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Refresh OAuth tokens for an MCP server
|
|
164
|
+
*/
|
|
165
|
+
async _tool_refresh_tokens(request) {
|
|
166
|
+
const { serverUrl } = request.params;
|
|
167
|
+
try {
|
|
168
|
+
this.logger.info(`Refreshing OAuth tokens for ${serverUrl}`);
|
|
169
|
+
const tokens = await this.storage.getTokens(serverUrl);
|
|
170
|
+
if (!tokens?.refresh_token) {
|
|
171
|
+
throw new Error('No refresh token available');
|
|
172
|
+
}
|
|
173
|
+
const serviceName = this.getServiceName(serverUrl);
|
|
174
|
+
const refreshResponse = await this.use(this.oauthToolAddress, {
|
|
175
|
+
method: 'refreshToken',
|
|
176
|
+
params: {
|
|
177
|
+
serviceName,
|
|
178
|
+
refreshToken: tokens.refresh_token,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const newTokens = refreshResponse.result?.data?.tokens ||
|
|
182
|
+
refreshResponse.result?.tokens;
|
|
183
|
+
// Save new tokens to encrypted storage
|
|
184
|
+
await this.storage.saveTokens(serverUrl, newTokens);
|
|
185
|
+
this.logger.info(`Tokens refreshed successfully for ${serverUrl}`);
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
message: 'Tokens refreshed successfully',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
this.logger.error(`Token refresh failed: ${error.message}`);
|
|
193
|
+
throw new Error(`Token refresh failed: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check OAuth token status for an MCP server
|
|
198
|
+
*/
|
|
199
|
+
async _tool_check_tokens(request) {
|
|
200
|
+
const { serverUrl } = request.params;
|
|
201
|
+
const tokens = await this.storage.getTokens(serverUrl);
|
|
202
|
+
const isExpired = await this.storage.areTokensExpired(serverUrl);
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
hasTokens: !!tokens,
|
|
206
|
+
isExpired: tokens ? isExpired : null,
|
|
207
|
+
expiresAt: tokens?.expires_at || null,
|
|
208
|
+
hasRefreshToken: !!tokens?.refresh_token,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Clear OAuth tokens and client info for an MCP server
|
|
213
|
+
*/
|
|
214
|
+
async _tool_clear_tokens(request) {
|
|
215
|
+
const { serverUrl } = request.params;
|
|
216
|
+
await this.storage.clearServer(serverUrl);
|
|
217
|
+
this.logger.info(`Cleared OAuth data for ${serverUrl}`);
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
message: `Cleared OAuth data for ${serverUrl}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* List all authenticated MCP servers
|
|
225
|
+
*/
|
|
226
|
+
async _tool_list_authenticated_servers(request) {
|
|
227
|
+
// This would require maintaining an index of servers
|
|
228
|
+
// For now, return empty list
|
|
229
|
+
// TODO: Implement server list tracking in storage
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
servers: [],
|
|
233
|
+
message: 'Server list tracking not yet implemented',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Public method for refreshing tokens (used by transport)
|
|
238
|
+
*/
|
|
239
|
+
async refreshTokens(serverUrl) {
|
|
240
|
+
// await this._tool_refresh_tokens({
|
|
241
|
+
// jsonrpc: '2.0',
|
|
242
|
+
// method: 'refresh_tokens',
|
|
243
|
+
// id: Date.now().toString(),
|
|
244
|
+
// params: { serverUrl },
|
|
245
|
+
// state: {},
|
|
246
|
+
// peerId: this.peerId,
|
|
247
|
+
// source: this.address!,
|
|
248
|
+
// target: this.address!,
|
|
249
|
+
// } as oRequest);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get service name for OAuth tool
|
|
253
|
+
*/
|
|
254
|
+
getServiceName(serverUrl) {
|
|
255
|
+
return `mcp_${this.hashServerUrl(serverUrl)}`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Hash server URL for service name
|
|
259
|
+
*/
|
|
260
|
+
hashServerUrl(url) {
|
|
261
|
+
return Buffer.from(url)
|
|
262
|
+
.toString('base64')
|
|
263
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
264
|
+
.substring(0, 16);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Launch browser for OAuth authorization
|
|
268
|
+
*/
|
|
269
|
+
async launchBrowser(url) {
|
|
270
|
+
try {
|
|
271
|
+
const open = (await import('open')).default;
|
|
272
|
+
await open(url);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
this.logger.warn(`Could not automatically open browser: ${error.message}`);
|
|
276
|
+
this.logger.info(`Please open this URL in your browser: ${url}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Cleanup on shutdown
|
|
281
|
+
*/
|
|
282
|
+
async stop() {
|
|
283
|
+
if (this.callbackServer) {
|
|
284
|
+
await this.callbackServer.stop().catch(() => { });
|
|
285
|
+
this.callbackServer = null;
|
|
286
|
+
}
|
|
287
|
+
await super.stop();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { oAddress } from '@olane/o-core';
|
|
2
|
+
import { OAuthTokens } from './interfaces/oauth-tokens.interface.js';
|
|
3
|
+
import { OAuthClientInfo } from './interfaces/oauth-client-info.interface.js';
|
|
4
|
+
interface OAuthStorageConfig {
|
|
5
|
+
storageAddress: oAddress;
|
|
6
|
+
useFunction: (address: oAddress, request: any) => Promise<any>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* OAuth storage adapter that uses Olane OS o-storage for encrypted persistence
|
|
10
|
+
*
|
|
11
|
+
* This adapter provides a thin layer over o://secure storage for OAuth tokens
|
|
12
|
+
* and client information, with automatic encryption handled by the storage layer.
|
|
13
|
+
*/
|
|
14
|
+
export declare class McpOAuthStorage {
|
|
15
|
+
private storageAddress;
|
|
16
|
+
private use;
|
|
17
|
+
constructor(config: OAuthStorageConfig);
|
|
18
|
+
/**
|
|
19
|
+
* Save OAuth tokens for a server (encrypted via o://secure)
|
|
20
|
+
*/
|
|
21
|
+
saveTokens(serverUrl: string, tokens: OAuthTokens): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Get OAuth tokens for a server (decrypted automatically)
|
|
24
|
+
*/
|
|
25
|
+
getTokens(serverUrl: string): Promise<OAuthTokens | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Save OAuth client info for a server (encrypted)
|
|
28
|
+
*/
|
|
29
|
+
saveClientInfo(serverUrl: string, clientInfo: OAuthClientInfo): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get OAuth client info for a server (decrypted automatically)
|
|
32
|
+
*/
|
|
33
|
+
getClientInfo(serverUrl: string): Promise<OAuthClientInfo | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all OAuth data for a server
|
|
36
|
+
*/
|
|
37
|
+
clearServer(serverUrl: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Check if tokens exist for a server
|
|
40
|
+
*/
|
|
41
|
+
hasTokens(serverUrl: string): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Check if tokens are expired
|
|
44
|
+
*/
|
|
45
|
+
areTokensExpired(serverUrl: string): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the token key for storage
|
|
48
|
+
*/
|
|
49
|
+
private getTokenKey;
|
|
50
|
+
/**
|
|
51
|
+
* Get the client info key for storage
|
|
52
|
+
*/
|
|
53
|
+
private getClientKey;
|
|
54
|
+
/**
|
|
55
|
+
* Hash server URL for use as storage key
|
|
56
|
+
*/
|
|
57
|
+
private hashServerUrl;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=mcp-oauth-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-oauth-storage.d.ts","sourceRoot":"","sources":["../../../src/oauth/mcp-oauth-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAE9E,UAAU,kBAAkB;IAC1B,cAAc,EAAE,QAAQ,CAAC;IACzB,WAAW,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAChE;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,GAAG,CAAoD;gBAEnD,MAAM,EAAE,kBAAkB;IAKtC;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBvE;;OAEG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAkB/D;;OAEG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IAYhB;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAgBvE;;OAEG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBnD;;OAEG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAepD;;OAEG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY3D;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,aAAa;CAOtB"}
|