mcp-remote 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +14 -0
- package/package.json +14 -0
- package/sse-auth-client.ts +307 -0
- package/sse-auth-proxy.ts +323 -0
- package/tsconfig.json +17 -0
package/.prettierrc
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-remote",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
7
|
+
"@types/express": "^5.0.0",
|
|
8
|
+
"@types/node": "^22.13.10",
|
|
9
|
+
"express": "^4.21.2",
|
|
10
|
+
"open": "^10.1.0",
|
|
11
|
+
"prettier": "^3.5.3",
|
|
12
|
+
"tsx": "^4.19.3"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// sse-auth-client.ts - MCP Client with OAuth support
|
|
4
|
+
// Run with: npx tsx sse-auth-client.ts sse-auth-client.ts https://example.remote/server [callback-port]
|
|
5
|
+
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import open from 'open'
|
|
8
|
+
import fs from 'fs/promises'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import os from 'os'
|
|
11
|
+
import crypto from 'crypto'
|
|
12
|
+
import { EventEmitter } from 'events'
|
|
13
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
14
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
|
15
|
+
import { OAuthClientProvider, auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
|
16
|
+
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
17
|
+
import {
|
|
18
|
+
OAuthClientInformation,
|
|
19
|
+
OAuthClientInformationFull,
|
|
20
|
+
OAuthClientInformationSchema,
|
|
21
|
+
OAuthTokens,
|
|
22
|
+
OAuthTokensSchema,
|
|
23
|
+
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
|
24
|
+
|
|
25
|
+
// Implement OAuth client provider for Node.js environment
|
|
26
|
+
class NodeOAuthClientProvider implements OAuthClientProvider {
|
|
27
|
+
private configDir: string
|
|
28
|
+
private serverUrlHash: string
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private serverUrl: string,
|
|
32
|
+
private callbackPort: number = 3333,
|
|
33
|
+
private callbackPath: string = '/oauth/callback',
|
|
34
|
+
) {
|
|
35
|
+
this.serverUrlHash = crypto.createHash('md5').update(serverUrl).digest('hex')
|
|
36
|
+
this.configDir = path.join(os.homedir(), '.mcp-auth')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get redirectUrl(): string {
|
|
40
|
+
return `http://localhost:${this.callbackPort}${this.callbackPath}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get clientMetadata() {
|
|
44
|
+
return {
|
|
45
|
+
redirect_uris: [this.redirectUrl],
|
|
46
|
+
token_endpoint_auth_method: 'none',
|
|
47
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
48
|
+
response_types: ['code'],
|
|
49
|
+
client_name: 'MCP CLI Client',
|
|
50
|
+
client_uri: 'https://github.com/modelcontextprotocol/mcp-cli',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async ensureConfigDir() {
|
|
55
|
+
try {
|
|
56
|
+
await fs.mkdir(this.configDir, { recursive: true })
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error creating config directory:', error)
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async readFile<T>(filename: string, schema: any): Promise<T | undefined> {
|
|
64
|
+
try {
|
|
65
|
+
await this.ensureConfigDir()
|
|
66
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
67
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
68
|
+
return await schema.parseAsync(JSON.parse(content))
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async writeFile(filename: string, data: any) {
|
|
78
|
+
try {
|
|
79
|
+
await this.ensureConfigDir()
|
|
80
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
81
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error writing ${filename}:`, error)
|
|
84
|
+
throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async writeTextFile(filename: string, text: string) {
|
|
89
|
+
try {
|
|
90
|
+
await this.ensureConfigDir()
|
|
91
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
92
|
+
await fs.writeFile(filePath, text, 'utf-8')
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`Error writing ${filename}:`, error)
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async readTextFile(filename: string): Promise<string> {
|
|
100
|
+
try {
|
|
101
|
+
await this.ensureConfigDir()
|
|
102
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
103
|
+
return await fs.readFile(filePath, 'utf-8')
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error('No code verifier saved for session')
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
|
110
|
+
return this.readFile<OAuthClientInformation>('client_info.json', OAuthClientInformationSchema)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
|
114
|
+
await this.writeFile('client_info.json', clientInformation)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async tokens(): Promise<OAuthTokens | undefined> {
|
|
118
|
+
return this.readFile<OAuthTokens>('tokens.json', OAuthTokensSchema)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
122
|
+
await this.writeFile('tokens.json', tokens)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
126
|
+
console.log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
|
|
127
|
+
try {
|
|
128
|
+
await open(authorizationUrl.toString())
|
|
129
|
+
console.log('Browser opened automatically.')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log('Could not open browser automatically. Please copy and paste the URL above into your browser.')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
|
136
|
+
await this.writeTextFile('code_verifier.txt', codeVerifier)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async codeVerifier(): Promise<string> {
|
|
140
|
+
return await this.readTextFile('code_verifier.txt')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Main function to run the client
|
|
145
|
+
async function runClient(serverUrl: string, callbackPort: number) {
|
|
146
|
+
// Set up event emitter for auth flow
|
|
147
|
+
const events = new EventEmitter()
|
|
148
|
+
|
|
149
|
+
// Create the OAuth client provider
|
|
150
|
+
const authProvider = new NodeOAuthClientProvider(serverUrl, callbackPort)
|
|
151
|
+
|
|
152
|
+
// Create the client
|
|
153
|
+
const client = new Client(
|
|
154
|
+
{
|
|
155
|
+
name: 'mcp-cli',
|
|
156
|
+
version: '0.1.0',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
capabilities: {
|
|
160
|
+
sampling: {},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Create the transport
|
|
166
|
+
const url = new URL(serverUrl)
|
|
167
|
+
|
|
168
|
+
function initTransport() {
|
|
169
|
+
const transport = new SSEClientTransport(url, { authProvider })
|
|
170
|
+
|
|
171
|
+
// Set up message and error handlers
|
|
172
|
+
transport.onmessage = (message) => {
|
|
173
|
+
console.log('Received message:', JSON.stringify(message, null, 2))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
transport.onerror = (error) => {
|
|
177
|
+
console.error('Transport error:', error)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
transport.onclose = () => {
|
|
181
|
+
console.log('Connection closed.')
|
|
182
|
+
process.exit(0)
|
|
183
|
+
}
|
|
184
|
+
return transport
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const transport = initTransport()
|
|
188
|
+
|
|
189
|
+
// Set up an HTTP server to handle OAuth callback
|
|
190
|
+
let authCode: string | null = null
|
|
191
|
+
const app = express()
|
|
192
|
+
|
|
193
|
+
app.get('/oauth/callback', (req, res) => {
|
|
194
|
+
const code = req.query.code as string | undefined
|
|
195
|
+
if (!code) {
|
|
196
|
+
res.status(400).send('Error: No authorization code received')
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
authCode = code
|
|
201
|
+
res.send('Authorization successful! You may close this window and return to the CLI.')
|
|
202
|
+
|
|
203
|
+
// Notify main flow that auth code is available
|
|
204
|
+
events.emit('auth-code-received', code)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const server = app.listen(callbackPort, () => {
|
|
208
|
+
console.log(`OAuth callback server running at http://localhost:${callbackPort}`)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Function to wait for auth code
|
|
212
|
+
const waitForAuthCode = (): Promise<string> => {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
if (authCode) {
|
|
215
|
+
resolve(authCode)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
events.once('auth-code-received', (code) => {
|
|
220
|
+
resolve(code)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Try to connect
|
|
226
|
+
try {
|
|
227
|
+
console.log('Connecting to server...')
|
|
228
|
+
await client.connect(transport)
|
|
229
|
+
console.log('Connected successfully!')
|
|
230
|
+
|
|
231
|
+
// Send a resources/list request
|
|
232
|
+
// console.log("Requesting resource list...");
|
|
233
|
+
// const result = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
234
|
+
// console.log("Resources:", JSON.stringify(result, null, 2));
|
|
235
|
+
|
|
236
|
+
console.log('Request tools list...')
|
|
237
|
+
const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
|
|
238
|
+
console.log('Tools:', JSON.stringify(tools, null, 2))
|
|
239
|
+
|
|
240
|
+
console.log('Listening for messages. Press Ctrl+C to exit.')
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
|
|
243
|
+
console.log('Authentication required. Waiting for authorization...')
|
|
244
|
+
|
|
245
|
+
// Wait for the authorization code from the callback
|
|
246
|
+
const code = await waitForAuthCode()
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
console.log('Completing authorization...')
|
|
250
|
+
await transport.finishAuth(code)
|
|
251
|
+
|
|
252
|
+
// Start a new transport here? Ok cause it's going to write to the file maybe?
|
|
253
|
+
|
|
254
|
+
// Reconnect after authorization
|
|
255
|
+
console.log('Connecting after authorization...')
|
|
256
|
+
await client.connect(initTransport())
|
|
257
|
+
|
|
258
|
+
console.log('Connected successfully!')
|
|
259
|
+
|
|
260
|
+
// // Send a resources/list request
|
|
261
|
+
// console.log("Requesting resource list...");
|
|
262
|
+
// const result = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
263
|
+
// console.log("Resources:", JSON.stringify(result, null, 2));2));
|
|
264
|
+
|
|
265
|
+
console.log('Request tools list...')
|
|
266
|
+
const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
|
|
267
|
+
console.log('Tools:', JSON.stringify(tools, null, 2))
|
|
268
|
+
|
|
269
|
+
console.log('Listening for messages. Press Ctrl+C to exit.')
|
|
270
|
+
} catch (authError) {
|
|
271
|
+
console.error('Authorization error:', authError)
|
|
272
|
+
server.close()
|
|
273
|
+
process.exit(1)
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
console.error('Connection error:', error)
|
|
277
|
+
server.close()
|
|
278
|
+
process.exit(1)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle shutdown
|
|
283
|
+
process.on('SIGINT', async () => {
|
|
284
|
+
console.log('\nClosing connection...')
|
|
285
|
+
await client.close()
|
|
286
|
+
server.close()
|
|
287
|
+
process.exit(0)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Keep the process alive
|
|
291
|
+
process.stdin.resume()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Parse command-line arguments
|
|
295
|
+
const args = process.argv.slice(2)
|
|
296
|
+
const serverUrl = args[0]
|
|
297
|
+
const callbackPort = args[1] ? parseInt(args[1]) : 3333
|
|
298
|
+
|
|
299
|
+
if (!serverUrl || !serverUrl.startsWith('https://')) {
|
|
300
|
+
console.error('Usage: node --experimental-strip-types sse-auth-client.ts <https://server-url> [callback-port]')
|
|
301
|
+
process.exit(1)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
runClient(serverUrl, callbackPort).catch((error) => {
|
|
305
|
+
console.error('Fatal error:', error)
|
|
306
|
+
process.exit(1)
|
|
307
|
+
})
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// sse-auth-proxy.ts - MCP Proxy with OAuth support
|
|
4
|
+
// Run with: npx tsx sse-auth-proxy.ts https://example.remote/server [callback-port]
|
|
5
|
+
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import open from 'open'
|
|
8
|
+
import fs from 'fs/promises'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import crypto from 'crypto'
|
|
11
|
+
import { EventEmitter } from 'events'
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
14
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
|
15
|
+
import { OAuthClientProvider, auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
|
16
|
+
import {
|
|
17
|
+
OAuthClientInformation,
|
|
18
|
+
OAuthClientInformationFull,
|
|
19
|
+
OAuthClientInformationSchema,
|
|
20
|
+
OAuthTokens,
|
|
21
|
+
OAuthTokensSchema,
|
|
22
|
+
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
|
23
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
24
|
+
import os from 'os'
|
|
25
|
+
|
|
26
|
+
// Implement OAuth client provider for Node.js environment
|
|
27
|
+
class NodeOAuthClientProvider implements OAuthClientProvider {
|
|
28
|
+
private configDir: string
|
|
29
|
+
private serverUrlHash: string
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private serverUrl: string,
|
|
33
|
+
private callbackPort: number = 3334,
|
|
34
|
+
private callbackPath: string = '/oauth/callback',
|
|
35
|
+
) {
|
|
36
|
+
this.serverUrlHash = crypto.createHash('md5').update(serverUrl).digest('hex')
|
|
37
|
+
this.configDir = path.join(os.homedir(), '.mcp-auth')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get redirectUrl(): string {
|
|
41
|
+
return `http://localhost:${this.callbackPort}${this.callbackPath}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get clientMetadata() {
|
|
45
|
+
return {
|
|
46
|
+
redirect_uris: [this.redirectUrl],
|
|
47
|
+
token_endpoint_auth_method: 'none',
|
|
48
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
49
|
+
response_types: ['code'],
|
|
50
|
+
client_name: 'MCP CLI Proxy',
|
|
51
|
+
client_uri: 'https://github.com/modelcontextprotocol/mcp-cli',
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async ensureConfigDir() {
|
|
56
|
+
try {
|
|
57
|
+
await fs.mkdir(this.configDir, { recursive: true })
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Error creating config directory:', error)
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async readFile<T>(filename: string, schema: any): Promise<T | undefined> {
|
|
65
|
+
try {
|
|
66
|
+
await this.ensureConfigDir()
|
|
67
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
68
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
69
|
+
return await schema.parseAsync(JSON.parse(content))
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async writeFile(filename: string, data: any) {
|
|
79
|
+
try {
|
|
80
|
+
await this.ensureConfigDir()
|
|
81
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
82
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Error writing ${filename}:`, error)
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async writeTextFile(filename: string, text: string) {
|
|
90
|
+
try {
|
|
91
|
+
await this.ensureConfigDir()
|
|
92
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
93
|
+
await fs.writeFile(filePath, text, 'utf-8')
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Error writing ${filename}:`, error)
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async readTextFile(filename: string): Promise<string> {
|
|
101
|
+
try {
|
|
102
|
+
await this.ensureConfigDir()
|
|
103
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
104
|
+
return await fs.readFile(filePath, 'utf-8')
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error('No code verifier saved for session')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
|
111
|
+
return this.readFile<OAuthClientInformation>('client_info.json', OAuthClientInformationSchema)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
|
115
|
+
await this.writeFile('client_info.json', clientInformation)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async tokens(): Promise<OAuthTokens | undefined> {
|
|
119
|
+
return this.readFile<OAuthTokens>('tokens.json', OAuthTokensSchema)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
123
|
+
await this.writeFile('tokens.json', tokens)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
127
|
+
console.error(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
|
|
128
|
+
try {
|
|
129
|
+
await open(authorizationUrl.toString())
|
|
130
|
+
console.error('Browser opened automatically.')
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Could not open browser automatically. Please copy and paste the URL above into your browser.')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
|
137
|
+
await this.writeTextFile('code_verifier.txt', codeVerifier)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async codeVerifier(): Promise<string> {
|
|
141
|
+
return await this.readTextFile('code_verifier.txt')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Function to proxy messages between two transports
|
|
146
|
+
function mcpProxy({ transportToClient, transportToServer }: { transportToClient: Transport; transportToServer: Transport }) {
|
|
147
|
+
let transportToClientClosed = false
|
|
148
|
+
let transportToServerClosed = false
|
|
149
|
+
|
|
150
|
+
transportToClient.onmessage = (message) => {
|
|
151
|
+
console.error('[Local→Remote]', message.method || message.id)
|
|
152
|
+
transportToServer.send(message).catch(onServerError)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
transportToServer.onmessage = (message) => {
|
|
156
|
+
console.error('[Remote→Local]', message.method || message.id)
|
|
157
|
+
transportToClient.send(message).catch(onClientError)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
transportToClient.onclose = () => {
|
|
161
|
+
if (transportToServerClosed) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
transportToClientClosed = true
|
|
166
|
+
transportToServer.close().catch(onServerError)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
transportToServer.onclose = () => {
|
|
170
|
+
if (transportToClientClosed) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
transportToServerClosed = true
|
|
174
|
+
transportToClient.close().catch(onClientError)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
transportToClient.onerror = onClientError
|
|
178
|
+
transportToServer.onerror = onServerError
|
|
179
|
+
|
|
180
|
+
function onClientError(error: Error) {
|
|
181
|
+
console.error('Error from local client:', error)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onServerError(error: Error) {
|
|
185
|
+
console.error('Error from remote server:', error)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Main function to run the proxy
|
|
190
|
+
async function runProxy(serverUrl: string, callbackPort: number) {
|
|
191
|
+
// Set up event emitter for auth flow
|
|
192
|
+
const events = new EventEmitter()
|
|
193
|
+
|
|
194
|
+
// Create the OAuth client provider
|
|
195
|
+
const authProvider = new NodeOAuthClientProvider(serverUrl, callbackPort)
|
|
196
|
+
|
|
197
|
+
// Create the STDIO transport
|
|
198
|
+
const localTransport = new StdioServerTransport()
|
|
199
|
+
|
|
200
|
+
// Set up an HTTP server to handle OAuth callback
|
|
201
|
+
let authCode: string | null = null
|
|
202
|
+
const app = express()
|
|
203
|
+
|
|
204
|
+
app.get('/oauth/callback', (req, res) => {
|
|
205
|
+
const code = req.query.code as string | undefined
|
|
206
|
+
if (!code) {
|
|
207
|
+
res.status(400).send('Error: No authorization code received')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
authCode = code
|
|
212
|
+
res.send('Authorization successful! You may close this window and return to the CLI.')
|
|
213
|
+
|
|
214
|
+
// Notify main flow that auth code is available
|
|
215
|
+
events.emit('auth-code-received', code)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const httpServer = app.listen(callbackPort, () => {
|
|
219
|
+
console.error(`OAuth callback server running at http://localhost:${callbackPort}`)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Function to wait for auth code
|
|
223
|
+
const waitForAuthCode = (): Promise<string> => {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
if (authCode) {
|
|
226
|
+
resolve(authCode)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
events.once('auth-code-received', (code) => {
|
|
231
|
+
resolve(code)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Function to create and connect to remote server, handling auth
|
|
237
|
+
const connectToRemoteServer = async (): Promise<SSEClientTransport> => {
|
|
238
|
+
console.error('Connecting to remote server:', serverUrl)
|
|
239
|
+
const url = new URL(serverUrl)
|
|
240
|
+
const transport = new SSEClientTransport(url, { authProvider })
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await transport.start()
|
|
244
|
+
console.error('Connected to remote server')
|
|
245
|
+
return transport
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
|
|
248
|
+
console.error('Authentication required. Waiting for authorization...')
|
|
249
|
+
|
|
250
|
+
// Wait for the authorization code from the callback
|
|
251
|
+
const code = await waitForAuthCode()
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
console.error('Completing authorization...')
|
|
255
|
+
await transport.finishAuth(code)
|
|
256
|
+
|
|
257
|
+
// Create a new transport after auth
|
|
258
|
+
const newTransport = new SSEClientTransport(url, { authProvider })
|
|
259
|
+
await newTransport.start()
|
|
260
|
+
console.error('Connected to remote server after authentication')
|
|
261
|
+
return newTransport
|
|
262
|
+
} catch (authError) {
|
|
263
|
+
console.error('Authorization error:', authError)
|
|
264
|
+
throw authError
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
console.error('Connection error:', error)
|
|
268
|
+
throw error
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Start local server
|
|
275
|
+
// await server.connect(serverTransport)
|
|
276
|
+
|
|
277
|
+
// Connect to remote server
|
|
278
|
+
const remoteTransport = await connectToRemoteServer()
|
|
279
|
+
|
|
280
|
+
// Set up bidirectional proxy
|
|
281
|
+
mcpProxy({
|
|
282
|
+
transportToClient: localTransport,
|
|
283
|
+
transportToServer: remoteTransport,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await localTransport.start()
|
|
287
|
+
console.error('Local STDIO server running')
|
|
288
|
+
|
|
289
|
+
console.error('Proxy established successfully')
|
|
290
|
+
console.error('Press Ctrl+C to exit')
|
|
291
|
+
|
|
292
|
+
// Handle shutdown
|
|
293
|
+
process.on('SIGINT', async () => {
|
|
294
|
+
console.error('\nShutting down proxy...')
|
|
295
|
+
await remoteTransport.close()
|
|
296
|
+
await localTransport.close()
|
|
297
|
+
httpServer.close()
|
|
298
|
+
process.exit(0)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Keep the process alive
|
|
302
|
+
process.stdin.resume()
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('Fatal error:', error)
|
|
305
|
+
httpServer.close()
|
|
306
|
+
process.exit(1)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Parse command-line arguments
|
|
311
|
+
const args = process.argv.slice(2)
|
|
312
|
+
const serverUrl = args[0]
|
|
313
|
+
const callbackPort = args[1] ? parseInt(args[1]) : 3334
|
|
314
|
+
|
|
315
|
+
if (!serverUrl || !serverUrl.startsWith('https://')) {
|
|
316
|
+
console.error('Usage: npx tsx sse-auth-proxy.ts <https://server-url> [callback-port]')
|
|
317
|
+
process.exit(1)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
runProxy(serverUrl, callbackPort).catch((error) => {
|
|
321
|
+
console.error('Fatal error:', error)
|
|
322
|
+
process.exit(1)
|
|
323
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./build",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"lib": ["ES2022"],
|
|
11
|
+
"types": ["node"],
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["*.ts","src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "packages", "**/*.spec.ts"]
|
|
17
|
+
}
|