@wonderwhy-er/desktop-commander 0.2.12 → 0.2.14
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/README.md +23 -0
- package/dist/data/onboarding-prompts.json +22 -13
- package/dist/handlers/search-handlers.js +1 -0
- package/dist/index-oauth.d.ts +2 -0
- package/dist/index-oauth.js +201 -0
- package/dist/oauth/provider.d.ts +22 -0
- package/dist/oauth/provider.js +124 -0
- package/dist/oauth/server.d.ts +18 -0
- package/dist/oauth/server.js +160 -0
- package/dist/oauth/types.d.ts +54 -0
- package/dist/oauth/types.js +2 -0
- package/dist/search-manager.d.ts +1 -0
- package/dist/search-manager.js +4 -0
- package/dist/server.js +172 -9
- package/dist/tools/config.js +8 -6
- package/dist/tools/pdf-processor.d.ts +1 -0
- package/dist/tools/pdf-processor.js +3 -0
- package/dist/tools/prompts.d.ts +23 -0
- package/dist/tools/prompts.js +46 -39
- package/dist/tools/schemas.d.ts +6 -3
- package/dist/tools/schemas.js +8 -1
- package/dist/utils/usageTracker.d.ts +36 -0
- package/dist/utils/usageTracker.js +97 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Work with code and text, run processes, and automate tasks, going far beyond oth
|
|
|
22
22
|
## Table of Contents
|
|
23
23
|
- [Features](#features)
|
|
24
24
|
- [How to install](#how-to-install)
|
|
25
|
+
- [Getting Started](#getting-started)
|
|
25
26
|
- [Usage](#usage)
|
|
26
27
|
- [Handling Long-Running Commands](#handling-long-running-commands)
|
|
27
28
|
- [Work in Progress and TODOs](#roadmap)
|
|
@@ -378,6 +379,28 @@ Close and restart Claude Desktop to complete the removal.
|
|
|
378
379
|
**Need help?**
|
|
379
380
|
- Join our Discord community: https://discord.com/invite/kQ27sNnZr7
|
|
380
381
|
|
|
382
|
+
## Getting Started
|
|
383
|
+
|
|
384
|
+
Once Desktop Commander is installed and Claude Desktop is restarted, you're ready to supercharge your Claude experience!
|
|
385
|
+
|
|
386
|
+
### 🚀 New User Onboarding
|
|
387
|
+
|
|
388
|
+
Desktop Commander includes intelligent onboarding to help you discover what's possible:
|
|
389
|
+
|
|
390
|
+
**For New Users:** When you're just getting started (fewer than 10 successful commands), Claude will automatically offer helpful getting-started guidance and practical tutorials after you use Desktop Commander successfully.
|
|
391
|
+
|
|
392
|
+
**Request Help Anytime:** You can ask for onboarding assistance at any time by simply saying:
|
|
393
|
+
- *"Help me get started with Desktop Commander"*
|
|
394
|
+
- *"Show me Desktop Commander examples"*
|
|
395
|
+
- *"What can I do with Desktop Commander?"*
|
|
396
|
+
|
|
397
|
+
Claude will then show you beginner-friendly tutorials and examples, including:
|
|
398
|
+
- 📁 Organizing your Downloads folder automatically
|
|
399
|
+
- 📊 Analyzing CSV/Excel files with Python
|
|
400
|
+
- ⚙️ Setting up GitHub Actions CI/CD
|
|
401
|
+
- 🔍 Exploring and understanding codebases
|
|
402
|
+
- 🤖 Running interactive development environments
|
|
403
|
+
|
|
381
404
|
## Usage
|
|
382
405
|
|
|
383
406
|
The server provides a comprehensive set of tools organized into several categories:
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
"id": "onb_001",
|
|
7
7
|
"title": "Organize my Downloads folder",
|
|
8
8
|
"description": "Clean up and organize your messy Downloads folder into relevant subfolders automatically.",
|
|
9
|
-
"prompt": "
|
|
9
|
+
"prompt": "Let's organize your Downloads folder! \n\nFirst, let me check what we're working with. I'll look at your Downloads folder to see how many files are there and what types.\n\nShould I start by analyzing your Downloads folder?",
|
|
10
10
|
"categories": ["onboarding"],
|
|
11
|
+
"secondaryTag": "Quick Start",
|
|
11
12
|
"votes": 0,
|
|
12
13
|
"gaClicks": 0,
|
|
13
14
|
"icon": "FolderOpen",
|
|
@@ -16,13 +17,14 @@
|
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
"id": "onb_002",
|
|
19
|
-
"title": "
|
|
20
|
-
"description": "
|
|
21
|
-
"prompt": "
|
|
20
|
+
"title": "Set up GitHub Actions CI/CD",
|
|
21
|
+
"description": "Set up GitHub Actions for your project to automatically run tests on every push with proper CI/CD workflow.",
|
|
22
|
+
"prompt": "Let's set up GitHub Actions CI/CD for your project! 🚀\n\n**What's the path to your project folder?**\n\n*Try: `~/work/my-project` (replace with your path) or give me a different path.*\n\nI'll analyze your project type and set up automated testing and deployment in about 15 minutes!",
|
|
22
23
|
"categories": ["onboarding"],
|
|
24
|
+
"secondaryTag": "Build & Deploy",
|
|
23
25
|
"votes": 0,
|
|
24
26
|
"gaClicks": 0,
|
|
25
|
-
"icon": "
|
|
27
|
+
"icon": "GitBranch",
|
|
26
28
|
"author": "DC team",
|
|
27
29
|
"verified": true
|
|
28
30
|
},
|
|
@@ -30,8 +32,9 @@
|
|
|
30
32
|
"id": "onb_003",
|
|
31
33
|
"title": "Create organized knowledge/documents folder",
|
|
32
34
|
"description": "Set up a well-structured knowledge base or document organization system with templates and suggested categories.",
|
|
33
|
-
"prompt": "
|
|
35
|
+
"prompt": "Let's create an organized knowledge base! 📚\n\n**Where should I set it up?**\n\n*I suggest: `~/Documents/Knowledge-Base` (replace with your path) or give me a different location.*\n\nI'll create a clean folder structure with templates and organize any existing documents you have!",
|
|
34
36
|
"categories": ["onboarding"],
|
|
37
|
+
"secondaryTag": "Quick Start",
|
|
35
38
|
"votes": 0,
|
|
36
39
|
"gaClicks": 0,
|
|
37
40
|
"icon": "BookOpen",
|
|
@@ -42,8 +45,9 @@
|
|
|
42
45
|
"id": "onb_004",
|
|
43
46
|
"title": "Explain codebase or repository to me",
|
|
44
47
|
"description": "Analyze and explain any codebase - local project or GitHub repository - including architecture, dependencies, and how it works.",
|
|
45
|
-
"prompt": "I'll
|
|
48
|
+
"prompt": "I'll analyze and explain any codebase for you! 🔍\n\n**What should I analyze?**\n\n*Local project:* `~/work/my-project` (replace with your path)\n*GitHub repo:* `https://github.com/user/repo`\n\nI'll break down the architecture, dependencies, and how everything works together!",
|
|
46
49
|
"categories": ["onboarding"],
|
|
50
|
+
"secondaryTag": "Code Analysis",
|
|
47
51
|
"votes": 0,
|
|
48
52
|
"gaClicks": 0,
|
|
49
53
|
"icon": "Code",
|
|
@@ -54,8 +58,9 @@
|
|
|
54
58
|
"id": "onb_005",
|
|
55
59
|
"title": "Clean up unused code in my project",
|
|
56
60
|
"description": "Scan your codebase to find unused imports, dead functions, and redundant code that can be safely removed.",
|
|
57
|
-
"prompt": "
|
|
61
|
+
"prompt": "Let's clean up unused code in your project! 🧹\n\n**What's your project folder path?**\n\n*Try: `~/work/my-project` (replace with your path)*\n\nI'll safely scan for dead code and unused imports, then show you exactly what can be removed before making any changes!",
|
|
58
62
|
"categories": ["onboarding"],
|
|
63
|
+
"secondaryTag": "Code Analysis",
|
|
59
64
|
"votes": 0,
|
|
60
65
|
"gaClicks": 0,
|
|
61
66
|
"icon": "Trash2",
|
|
@@ -65,9 +70,10 @@
|
|
|
65
70
|
{
|
|
66
71
|
"id": "onb_006",
|
|
67
72
|
"title": "Build shopping list app and deploy online",
|
|
68
|
-
"description": "Create a
|
|
69
|
-
"prompt": "Let's build
|
|
73
|
+
"description": "Create a simple shopping list web app from scratch and deploy it online - perfect for learning web development basics.",
|
|
74
|
+
"prompt": "Let's build a simple shopping list web app and deploy it online! 🛒\n\n**Quick question:** Where should I create the project folder?\n\n*I suggest using `~/Downloads/shopping-app` for quick testing, or give me a different path if you prefer.*\n\nOnce I have the folder, I'll build a working app step-by-step and get it online in about 20 minutes!",
|
|
70
75
|
"categories": ["onboarding"],
|
|
76
|
+
"secondaryTag": "Build & Deploy",
|
|
71
77
|
"votes": 0,
|
|
72
78
|
"gaClicks": 0,
|
|
73
79
|
"icon": "ShoppingCart",
|
|
@@ -78,8 +84,9 @@
|
|
|
78
84
|
"id": "onb_007",
|
|
79
85
|
"title": "Analyze my data file",
|
|
80
86
|
"description": "Upload or point to any data file (CSV, JSON, Excel, etc.) and get comprehensive analysis including patterns, insights, and summary reports.",
|
|
81
|
-
"prompt": "I
|
|
87
|
+
"prompt": "I'll help you analyze your data file! \n\nWhat's the path to your data file? (e.g., `/Users/yourname/data.csv`)\n\nOnce you give me the path, I'll start by checking what type of file it is and show you a quick preview, then we can dive deeper step by step.",
|
|
82
88
|
"categories": ["onboarding"],
|
|
89
|
+
"secondaryTag": "Quick Start",
|
|
83
90
|
"votes": 0,
|
|
84
91
|
"gaClicks": 0,
|
|
85
92
|
"icon": "BarChart3",
|
|
@@ -90,8 +97,9 @@
|
|
|
90
97
|
"id": "onb_008",
|
|
91
98
|
"title": "Check system health and resources",
|
|
92
99
|
"description": "Analyze your system's health, resource usage, running processes, and generate a comprehensive system status report.",
|
|
93
|
-
"prompt": "Let me
|
|
100
|
+
"prompt": "Let me check your system health and resources!\n\nI'll start by looking at your CPU, memory, and disk usage, then check for any performance issues.\n\nShould I begin the system analysis?",
|
|
94
101
|
"categories": ["onboarding"],
|
|
102
|
+
"secondaryTag": "Quick Start",
|
|
95
103
|
"votes": 0,
|
|
96
104
|
"gaClicks": 0,
|
|
97
105
|
"icon": "Activity",
|
|
@@ -102,8 +110,9 @@
|
|
|
102
110
|
"id": "onb_009",
|
|
103
111
|
"title": "Find Patterns and Errors in Log Files",
|
|
104
112
|
"description": "Analyze log files to identify errors, patterns, performance issues, and security concerns with detailed insights and recommendations.",
|
|
105
|
-
"prompt": "I'll
|
|
113
|
+
"prompt": "I'll analyze your log files to find errors and patterns! 🔍\n\n**What log file should I analyze?**\n\n*Try: `/var/log/system.log` (macOS/Linux) or `~/app.log`, or I can search for logs on your system.*\n\nI'll find errors, performance issues, and suspicious patterns with actionable recommendations!",
|
|
106
114
|
"categories": ["onboarding"],
|
|
115
|
+
"secondaryTag": "Code Analysis",
|
|
107
116
|
"votes": 0,
|
|
108
117
|
"gaClicks": 0,
|
|
109
118
|
"icon": "Search",
|
|
@@ -24,6 +24,7 @@ export async function handleStartSearch(args) {
|
|
|
24
24
|
contextLines: parsed.data.contextLines,
|
|
25
25
|
timeout: parsed.data.timeout_ms,
|
|
26
26
|
earlyTermination: parsed.data.earlyTermination,
|
|
27
|
+
literalSearch: parsed.data.literalSearch,
|
|
27
28
|
});
|
|
28
29
|
const searchTypeText = parsed.data.searchType === 'content' ? 'content search' : 'file search';
|
|
29
30
|
let output = `Started ${searchTypeText} session: ${result.sessionId}\n`;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { FilteredStdioServerTransport } from './custom-stdio.js';
|
|
3
|
+
import { server } from './server.js';
|
|
4
|
+
import { configManager } from './config-manager.js';
|
|
5
|
+
import { runSetup } from './npm-scripts/setup.js';
|
|
6
|
+
import { runUninstall } from './npm-scripts/uninstall.js';
|
|
7
|
+
import { capture } from './utils/capture.js';
|
|
8
|
+
import { logToStderr, logger } from './utils/logger.js';
|
|
9
|
+
import { OAuthHttpServer } from './oauth/server.js';
|
|
10
|
+
async function runServer() {
|
|
11
|
+
try {
|
|
12
|
+
// Check if first argument is "setup"
|
|
13
|
+
if (process.argv[2] === 'setup') {
|
|
14
|
+
await runSetup();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Check if first argument is "remove"
|
|
18
|
+
if (process.argv[2] === 'remove') {
|
|
19
|
+
await runUninstall();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Check for HTTP mode with OAuth
|
|
23
|
+
const httpMode = process.argv.includes('--http') || process.argv.includes('--oauth');
|
|
24
|
+
const port = getPortFromArgs() || 8000;
|
|
25
|
+
if (httpMode) {
|
|
26
|
+
await runHttpServer(port);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
await runStdioServer();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger.error('Failed to start server:', error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function runStdioServer() {
|
|
38
|
+
logger.info('Loading server.ts');
|
|
39
|
+
logger.info('Setting up request handlers...');
|
|
40
|
+
try {
|
|
41
|
+
logger.info('Loading configuration...');
|
|
42
|
+
await configManager.loadConfig();
|
|
43
|
+
logger.info('Configuration loaded successfully');
|
|
44
|
+
const transport = new FilteredStdioServerTransport();
|
|
45
|
+
logger.info('Enhanced FilteredStdioServerTransport initialized');
|
|
46
|
+
logger.info('Connecting server...');
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
logger.info('Server connected successfully');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
await capture('error_start_stdio_server', { error });
|
|
52
|
+
logToStderr('error', `Failed to start stdio server: ${error}`);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function runHttpServer(port) {
|
|
57
|
+
logger.info(`Starting HTTP server with OAuth on port ${port}`);
|
|
58
|
+
try {
|
|
59
|
+
logger.info('Loading configuration...');
|
|
60
|
+
await configManager.loadConfig();
|
|
61
|
+
logger.info('Configuration loaded successfully');
|
|
62
|
+
// OAuth configuration
|
|
63
|
+
const baseUrl = `http://localhost:${port}`;
|
|
64
|
+
const oauthConfig = {
|
|
65
|
+
enabled: true,
|
|
66
|
+
clientId: 'desktop-commander-mcp',
|
|
67
|
+
clientSecret: 'dc-secret-' + Math.random().toString(36).substring(7),
|
|
68
|
+
redirectUri: `${baseUrl}/callback`,
|
|
69
|
+
authorizationUrl: `${baseUrl}/authorize`,
|
|
70
|
+
tokenUrl: `${baseUrl}/token`,
|
|
71
|
+
scope: 'mcp:access mcp:tools mcp:resources',
|
|
72
|
+
issuer: baseUrl
|
|
73
|
+
};
|
|
74
|
+
// Create MCP handler for HTTP requests
|
|
75
|
+
const mcpHandler = createMcpHttpHandler();
|
|
76
|
+
// Create OAuth HTTP server
|
|
77
|
+
const oauthServer = new OAuthHttpServer(oauthConfig, mcpHandler);
|
|
78
|
+
oauthServer.listen(port, () => {
|
|
79
|
+
logger.info(`HTTP server running on port ${port}`);
|
|
80
|
+
logger.info(`Authorization Server Metadata: ${baseUrl}/.well-known/oauth-authorization-server`);
|
|
81
|
+
logger.info(`MCP Endpoint: ${baseUrl}/mcp`);
|
|
82
|
+
logger.info(`SSE Endpoint: ${baseUrl}/sse`);
|
|
83
|
+
console.log(`\n🚀 DesktopCommanderMCP HTTP Server Started!`);
|
|
84
|
+
console.log(`📍 Server URL: ${baseUrl}`);
|
|
85
|
+
console.log(`🔐 OAuth Metadata: ${baseUrl}/.well-known/oauth-authorization-server`);
|
|
86
|
+
console.log(`🔧 MCP Endpoint: ${baseUrl}/mcp`);
|
|
87
|
+
console.log(`⚡ SSE Endpoint: ${baseUrl}/sse`);
|
|
88
|
+
console.log(`\n📝 For Claude Custom Connectors:`);
|
|
89
|
+
console.log(` URL: ${baseUrl}/sse`);
|
|
90
|
+
console.log(` Authentication: OAuth`);
|
|
91
|
+
});
|
|
92
|
+
// Graceful shutdown
|
|
93
|
+
process.on('SIGINT', () => {
|
|
94
|
+
logger.info('Received SIGINT, shutting down gracefully...');
|
|
95
|
+
oauthServer.close(() => {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await capture('error_start_http_server', { error });
|
|
102
|
+
logToStderr('error', `Failed to start HTTP server: ${error}`);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function createMcpHttpHandler() {
|
|
107
|
+
return (req, res) => {
|
|
108
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
109
|
+
// Handle SSE endpoint for MCP clients
|
|
110
|
+
if (url.pathname === '/sse') {
|
|
111
|
+
handleSSE(req, res);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Handle Streamable HTTP endpoint
|
|
115
|
+
if (url.pathname === '/mcp') {
|
|
116
|
+
handleStreamableHttp(req, res);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Health check
|
|
120
|
+
if (url.pathname === '/health') {
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ status: 'ok', service: 'DesktopCommanderMCP' }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// 404 for other paths
|
|
126
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function handleSSE(req, res) {
|
|
131
|
+
// Set SSE headers
|
|
132
|
+
res.writeHead(200, {
|
|
133
|
+
'Content-Type': 'text/event-stream',
|
|
134
|
+
'Cache-Control': 'no-cache',
|
|
135
|
+
'Connection': 'keep-alive',
|
|
136
|
+
'Access-Control-Allow-Origin': '*',
|
|
137
|
+
'Access-Control-Allow-Headers': 'Cache-Control'
|
|
138
|
+
});
|
|
139
|
+
// SSE implementation would go here
|
|
140
|
+
// For now, just indicate that SSE is available
|
|
141
|
+
res.write('event: connected\n');
|
|
142
|
+
res.write('data: {"type":"connected","message":"DesktopCommanderMCP SSE endpoint"}\n\n');
|
|
143
|
+
// Keep connection alive
|
|
144
|
+
const heartbeat = setInterval(() => {
|
|
145
|
+
res.write('event: heartbeat\n');
|
|
146
|
+
res.write('data: {"type":"heartbeat","timestamp":' + Date.now() + '}\n\n');
|
|
147
|
+
}, 30000);
|
|
148
|
+
req.on('close', () => {
|
|
149
|
+
clearInterval(heartbeat);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function handleStreamableHttp(req, res) {
|
|
153
|
+
// Handle Streamable HTTP for MCP
|
|
154
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
155
|
+
if (req.method === 'GET') {
|
|
156
|
+
// Return server info
|
|
157
|
+
res.end(JSON.stringify({
|
|
158
|
+
name: 'DesktopCommanderMCP',
|
|
159
|
+
version: '0.2.13',
|
|
160
|
+
transport: 'streamable-http',
|
|
161
|
+
authenticated: !!req.user
|
|
162
|
+
}));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Handle MCP protocol messages
|
|
166
|
+
if (req.method === 'POST') {
|
|
167
|
+
let body = '';
|
|
168
|
+
req.on('data', chunk => body += chunk.toString());
|
|
169
|
+
req.on('end', () => {
|
|
170
|
+
try {
|
|
171
|
+
const message = JSON.parse(body);
|
|
172
|
+
// Process MCP message through the server
|
|
173
|
+
// This would need to be integrated with the existing MCP server
|
|
174
|
+
res.end(JSON.stringify({
|
|
175
|
+
jsonrpc: '2.0',
|
|
176
|
+
id: message.id,
|
|
177
|
+
result: { message: 'MCP message received' }
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
182
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
189
|
+
}
|
|
190
|
+
function getPortFromArgs() {
|
|
191
|
+
const portIndex = process.argv.findIndex(arg => arg === '--port');
|
|
192
|
+
if (portIndex !== -1 && process.argv[portIndex + 1]) {
|
|
193
|
+
return parseInt(process.argv[portIndex + 1], 10);
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
// Run the server
|
|
198
|
+
runServer().catch(error => {
|
|
199
|
+
console.error('Fatal error:', error);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OAuthConfig, AuthorizationServerMetadata, TokenResponse, AccessToken, AuthorizationRequest, TokenRequest } from './types.js';
|
|
2
|
+
export declare class OAuthProvider {
|
|
3
|
+
private config;
|
|
4
|
+
private users;
|
|
5
|
+
private authCodes;
|
|
6
|
+
private accessTokens;
|
|
7
|
+
constructor(config: OAuthConfig);
|
|
8
|
+
static generatePKCE(): {
|
|
9
|
+
codeVerifier: string;
|
|
10
|
+
codeChallenge: string;
|
|
11
|
+
};
|
|
12
|
+
getAuthorizationServerMetadata(): AuthorizationServerMetadata;
|
|
13
|
+
handleAuthorizationRequest(params: AuthorizationRequest): {
|
|
14
|
+
authUrl: string;
|
|
15
|
+
authCode?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
handleTokenRequest(params: TokenRequest): Promise<TokenResponse | {
|
|
19
|
+
error: string;
|
|
20
|
+
}>;
|
|
21
|
+
validateAccessToken(token: string): AccessToken | null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export class OAuthProvider {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.users = new Map();
|
|
5
|
+
this.authCodes = new Map();
|
|
6
|
+
this.accessTokens = new Map();
|
|
7
|
+
this.config = config;
|
|
8
|
+
// Add a default user for testing
|
|
9
|
+
this.users.set('admin', {
|
|
10
|
+
email: 'admin@localhost',
|
|
11
|
+
password: 'admin123' // In production, hash this!
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Generate PKCE code verifier and challenge
|
|
15
|
+
static generatePKCE() {
|
|
16
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
17
|
+
const codeChallenge = crypto
|
|
18
|
+
.createHash('sha256')
|
|
19
|
+
.update(codeVerifier)
|
|
20
|
+
.digest('base64url');
|
|
21
|
+
return { codeVerifier, codeChallenge };
|
|
22
|
+
}
|
|
23
|
+
// Get Authorization Server Metadata (required by MCP spec)
|
|
24
|
+
getAuthorizationServerMetadata() {
|
|
25
|
+
const baseUrl = this.config.issuer;
|
|
26
|
+
return {
|
|
27
|
+
issuer: baseUrl,
|
|
28
|
+
authorization_endpoint: `${baseUrl}/authorize`,
|
|
29
|
+
token_endpoint: `${baseUrl}/token`,
|
|
30
|
+
registration_endpoint: `${baseUrl}/register`,
|
|
31
|
+
revocation_endpoint: `${baseUrl}/revoke`,
|
|
32
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
33
|
+
response_types_supported: ['code'],
|
|
34
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
35
|
+
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic'],
|
|
36
|
+
code_challenge_methods_supported: ['S256'],
|
|
37
|
+
scopes_supported: ['mcp:access', 'mcp:tools', 'mcp:resources']
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Handle authorization request
|
|
41
|
+
handleAuthorizationRequest(params) {
|
|
42
|
+
if (!params.client_id || !params.redirect_uri || !params.code_challenge) {
|
|
43
|
+
return { authUrl: '', error: 'Missing required parameters' };
|
|
44
|
+
}
|
|
45
|
+
if (params.code_challenge_method !== 'S256') {
|
|
46
|
+
return { authUrl: '', error: 'Unsupported code_challenge_method' };
|
|
47
|
+
}
|
|
48
|
+
// Generate authorization code
|
|
49
|
+
const authCode = crypto.randomBytes(32).toString('hex');
|
|
50
|
+
const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
|
|
51
|
+
// Store authorization code
|
|
52
|
+
this.authCodes.set(authCode, {
|
|
53
|
+
userId: 'admin', // For simplicity, auto-approve for admin user
|
|
54
|
+
clientId: params.client_id,
|
|
55
|
+
redirectUri: params.redirect_uri,
|
|
56
|
+
scope: params.scope || 'mcp:access',
|
|
57
|
+
codeChallenge: params.code_challenge,
|
|
58
|
+
codeChallengeMethod: params.code_challenge_method,
|
|
59
|
+
expiresAt
|
|
60
|
+
});
|
|
61
|
+
// Build redirect URL with auth code
|
|
62
|
+
const redirectUrl = new URL(params.redirect_uri);
|
|
63
|
+
redirectUrl.searchParams.set('code', authCode);
|
|
64
|
+
if (params.state) {
|
|
65
|
+
redirectUrl.searchParams.set('state', params.state);
|
|
66
|
+
}
|
|
67
|
+
return { authUrl: redirectUrl.toString(), authCode };
|
|
68
|
+
}
|
|
69
|
+
// Exchange authorization code for access token
|
|
70
|
+
async handleTokenRequest(params) {
|
|
71
|
+
const authCodeData = this.authCodes.get(params.code);
|
|
72
|
+
if (!authCodeData) {
|
|
73
|
+
return { error: 'Invalid authorization code' };
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() > authCodeData.expiresAt) {
|
|
76
|
+
this.authCodes.delete(params.code);
|
|
77
|
+
return { error: 'Authorization code expired' };
|
|
78
|
+
}
|
|
79
|
+
// Verify PKCE
|
|
80
|
+
const computedChallenge = crypto
|
|
81
|
+
.createHash('sha256')
|
|
82
|
+
.update(params.code_verifier)
|
|
83
|
+
.digest('base64url');
|
|
84
|
+
if (computedChallenge !== authCodeData.codeChallenge) {
|
|
85
|
+
return { error: 'Invalid code_verifier' };
|
|
86
|
+
}
|
|
87
|
+
// Verify client and redirect URI
|
|
88
|
+
if (params.client_id !== authCodeData.clientId ||
|
|
89
|
+
params.redirect_uri !== authCodeData.redirectUri) {
|
|
90
|
+
return { error: 'Invalid client_id or redirect_uri' };
|
|
91
|
+
}
|
|
92
|
+
// Generate access token
|
|
93
|
+
const accessToken = crypto.randomBytes(32).toString('hex');
|
|
94
|
+
const expiresIn = 3600; // 1 hour
|
|
95
|
+
const tokenData = {
|
|
96
|
+
sub: authCodeData.userId,
|
|
97
|
+
aud: params.client_id,
|
|
98
|
+
iss: this.config.issuer,
|
|
99
|
+
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
|
100
|
+
iat: Math.floor(Date.now() / 1000),
|
|
101
|
+
scope: authCodeData.scope
|
|
102
|
+
};
|
|
103
|
+
this.accessTokens.set(accessToken, tokenData);
|
|
104
|
+
this.authCodes.delete(params.code);
|
|
105
|
+
return {
|
|
106
|
+
access_token: accessToken,
|
|
107
|
+
token_type: 'Bearer',
|
|
108
|
+
expires_in: expiresIn,
|
|
109
|
+
scope: authCodeData.scope
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Validate access token
|
|
113
|
+
validateAccessToken(token) {
|
|
114
|
+
const tokenData = this.accessTokens.get(token);
|
|
115
|
+
if (!tokenData) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (Date.now() / 1000 > tokenData.exp) {
|
|
119
|
+
this.accessTokens.delete(token);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return tokenData;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import type { OAuthConfig } from './types.js';
|
|
3
|
+
export declare class OAuthHttpServer {
|
|
4
|
+
private server;
|
|
5
|
+
private oauthProvider;
|
|
6
|
+
private mcpHandler;
|
|
7
|
+
constructor(config: OAuthConfig, mcpHandler: (req: http.IncomingMessage, res: http.ServerResponse) => void);
|
|
8
|
+
private handleRequest;
|
|
9
|
+
private setCorsHeaders;
|
|
10
|
+
private handleAuthServerMetadata;
|
|
11
|
+
private handleAuthorize;
|
|
12
|
+
private handleToken;
|
|
13
|
+
private handleCallback;
|
|
14
|
+
private sendUnauthorized;
|
|
15
|
+
private getRequestBody;
|
|
16
|
+
listen(port: number, callback?: () => void): void;
|
|
17
|
+
close(callback?: () => void): void;
|
|
18
|
+
}
|