@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { OAuthProvider } from './provider.js';
|
|
4
|
+
export class OAuthHttpServer {
|
|
5
|
+
constructor(config, mcpHandler) {
|
|
6
|
+
this.oauthProvider = new OAuthProvider(config);
|
|
7
|
+
this.mcpHandler = mcpHandler;
|
|
8
|
+
this.server = http.createServer(this.handleRequest.bind(this));
|
|
9
|
+
}
|
|
10
|
+
async handleRequest(req, res) {
|
|
11
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
12
|
+
// Enable CORS
|
|
13
|
+
this.setCorsHeaders(res);
|
|
14
|
+
if (req.method === 'OPTIONS') {
|
|
15
|
+
res.writeHead(200);
|
|
16
|
+
res.end();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// OAuth endpoints
|
|
21
|
+
if (url.pathname === '/.well-known/oauth-authorization-server') {
|
|
22
|
+
return this.handleAuthServerMetadata(res);
|
|
23
|
+
}
|
|
24
|
+
if (url.pathname === '/authorize') {
|
|
25
|
+
return this.handleAuthorize(url, res);
|
|
26
|
+
}
|
|
27
|
+
if (url.pathname === '/token' && req.method === 'POST') {
|
|
28
|
+
return this.handleToken(req, res);
|
|
29
|
+
}
|
|
30
|
+
if (url.pathname === '/callback') {
|
|
31
|
+
return this.handleCallback(url, res);
|
|
32
|
+
}
|
|
33
|
+
// Protected MCP endpoints - require authentication
|
|
34
|
+
if (url.pathname.startsWith('/mcp') || url.pathname === '/sse') {
|
|
35
|
+
const authHeader = req.headers.authorization;
|
|
36
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
37
|
+
return this.sendUnauthorized(res);
|
|
38
|
+
}
|
|
39
|
+
const token = authHeader.substring(7);
|
|
40
|
+
const tokenData = this.oauthProvider.validateAccessToken(token);
|
|
41
|
+
if (!tokenData) {
|
|
42
|
+
return this.sendUnauthorized(res);
|
|
43
|
+
}
|
|
44
|
+
// Add user info to request for MCP handler
|
|
45
|
+
req.user = tokenData;
|
|
46
|
+
}
|
|
47
|
+
// Forward to MCP handler
|
|
48
|
+
this.mcpHandler(req, res);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('OAuth server error:', error);
|
|
52
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
53
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
setCorsHeaders(res) {
|
|
57
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
58
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
59
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
60
|
+
res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');
|
|
61
|
+
}
|
|
62
|
+
handleAuthServerMetadata(res) {
|
|
63
|
+
const metadata = this.oauthProvider.getAuthorizationServerMetadata();
|
|
64
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(JSON.stringify(metadata, null, 2));
|
|
66
|
+
}
|
|
67
|
+
handleAuthorize(url, res) {
|
|
68
|
+
const params = {
|
|
69
|
+
response_type: url.searchParams.get('response_type'),
|
|
70
|
+
client_id: url.searchParams.get('client_id'),
|
|
71
|
+
redirect_uri: url.searchParams.get('redirect_uri'),
|
|
72
|
+
scope: url.searchParams.get('scope') || undefined,
|
|
73
|
+
state: url.searchParams.get('state') || undefined,
|
|
74
|
+
code_challenge: url.searchParams.get('code_challenge'),
|
|
75
|
+
code_challenge_method: url.searchParams.get('code_challenge_method')
|
|
76
|
+
};
|
|
77
|
+
const result = this.oauthProvider.handleAuthorizationRequest(params);
|
|
78
|
+
if (result.error) {
|
|
79
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Redirect to the callback URL with auth code
|
|
84
|
+
res.writeHead(302, { 'Location': result.authUrl });
|
|
85
|
+
res.end();
|
|
86
|
+
}
|
|
87
|
+
async handleToken(req, res) {
|
|
88
|
+
const body = await this.getRequestBody(req);
|
|
89
|
+
const params = new URLSearchParams(body);
|
|
90
|
+
const tokenRequest = {
|
|
91
|
+
grant_type: params.get('grant_type'),
|
|
92
|
+
code: params.get('code'),
|
|
93
|
+
redirect_uri: params.get('redirect_uri'),
|
|
94
|
+
client_id: params.get('client_id'),
|
|
95
|
+
code_verifier: params.get('code_verifier')
|
|
96
|
+
};
|
|
97
|
+
const result = await this.oauthProvider.handleTokenRequest(tokenRequest);
|
|
98
|
+
if ('error' in result) {
|
|
99
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
100
|
+
res.end(JSON.stringify(result));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify(result));
|
|
105
|
+
}
|
|
106
|
+
handleCallback(url, res) {
|
|
107
|
+
const code = url.searchParams.get('code');
|
|
108
|
+
const state = url.searchParams.get('state');
|
|
109
|
+
// Simple success page
|
|
110
|
+
const html = `
|
|
111
|
+
<!DOCTYPE html>
|
|
112
|
+
<html>
|
|
113
|
+
<head><title>Authorization Successful</title></head>
|
|
114
|
+
<body>
|
|
115
|
+
<h1>Authorization Successful!</h1>
|
|
116
|
+
<p>You can now close this window.</p>
|
|
117
|
+
<script>
|
|
118
|
+
// Try to close the window (works if opened by script)
|
|
119
|
+
try { window.close(); } catch(e) {}
|
|
120
|
+
|
|
121
|
+
// Post message to parent if in iframe
|
|
122
|
+
if (window.opener) {
|
|
123
|
+
window.opener.postMessage({
|
|
124
|
+
type: 'oauth_success',
|
|
125
|
+
code: '${code}',
|
|
126
|
+
state: '${state}'
|
|
127
|
+
}, '*');
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
`;
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
134
|
+
res.end(html);
|
|
135
|
+
}
|
|
136
|
+
sendUnauthorized(res) {
|
|
137
|
+
res.writeHead(401, {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'WWW-Authenticate': 'Bearer realm="mcp", error="invalid_token"'
|
|
140
|
+
});
|
|
141
|
+
res.end(JSON.stringify({
|
|
142
|
+
error: 'unauthorized',
|
|
143
|
+
message: 'Valid access token required'
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
async getRequestBody(req) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
let body = '';
|
|
149
|
+
req.on('data', chunk => body += chunk.toString());
|
|
150
|
+
req.on('end', () => resolve(body));
|
|
151
|
+
req.on('error', reject);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
listen(port, callback) {
|
|
155
|
+
this.server.listen(port, callback);
|
|
156
|
+
}
|
|
157
|
+
close(callback) {
|
|
158
|
+
this.server.close(callback);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface OAuthConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
authorizationUrl: string;
|
|
7
|
+
tokenUrl: string;
|
|
8
|
+
scope: string;
|
|
9
|
+
issuer: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthorizationServerMetadata {
|
|
12
|
+
issuer: string;
|
|
13
|
+
authorization_endpoint: string;
|
|
14
|
+
token_endpoint: string;
|
|
15
|
+
registration_endpoint?: string;
|
|
16
|
+
revocation_endpoint?: string;
|
|
17
|
+
jwks_uri?: string;
|
|
18
|
+
response_types_supported: string[];
|
|
19
|
+
grant_types_supported: string[];
|
|
20
|
+
token_endpoint_auth_methods_supported: string[];
|
|
21
|
+
code_challenge_methods_supported: string[];
|
|
22
|
+
scopes_supported?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface TokenResponse {
|
|
25
|
+
access_token: string;
|
|
26
|
+
token_type: string;
|
|
27
|
+
expires_in?: number;
|
|
28
|
+
refresh_token?: string;
|
|
29
|
+
scope?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface AccessToken {
|
|
32
|
+
sub: string;
|
|
33
|
+
aud: string | string[];
|
|
34
|
+
iss: string;
|
|
35
|
+
exp: number;
|
|
36
|
+
iat: number;
|
|
37
|
+
scope?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface AuthorizationRequest {
|
|
40
|
+
response_type: 'code';
|
|
41
|
+
client_id: string;
|
|
42
|
+
redirect_uri: string;
|
|
43
|
+
scope?: string;
|
|
44
|
+
state?: string;
|
|
45
|
+
code_challenge: string;
|
|
46
|
+
code_challenge_method: 'S256';
|
|
47
|
+
}
|
|
48
|
+
export interface TokenRequest {
|
|
49
|
+
grant_type: 'authorization_code';
|
|
50
|
+
code: string;
|
|
51
|
+
redirect_uri: string;
|
|
52
|
+
client_id: string;
|
|
53
|
+
code_verifier: string;
|
|
54
|
+
}
|
package/dist/search-manager.d.ts
CHANGED
package/dist/search-manager.js
CHANGED
|
@@ -212,6 +212,10 @@ import { capture } from './utils/capture.js';
|
|
|
212
212
|
if (options.searchType === 'content') {
|
|
213
213
|
// Content search mode
|
|
214
214
|
args.push('--json', '--line-number');
|
|
215
|
+
// Add literal search support for content searches
|
|
216
|
+
if (options.literalSearch) {
|
|
217
|
+
args.push('-F'); // Fixed string matching (literal)
|
|
218
|
+
}
|
|
215
219
|
if (options.contextLines && options.contextLines > 0) {
|
|
216
220
|
args.push('-C', options.contextLines.toString());
|
|
217
221
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
4
|
import { getSystemInfo, getOSSpecificGuidance, getPathGuidance, getDevelopmentToolGuidance } from './utils/system-info.js';
|
|
5
5
|
// Get system information once at startup
|
|
@@ -257,24 +257,72 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
257
257
|
description: `
|
|
258
258
|
Start a streaming search that can return results progressively.
|
|
259
259
|
|
|
260
|
+
SEARCH STRATEGY GUIDE:
|
|
261
|
+
Choose the right search type based on what the user is looking for:
|
|
262
|
+
|
|
263
|
+
USE searchType="files" WHEN:
|
|
264
|
+
- User asks for specific files: "find package.json", "locate config files"
|
|
265
|
+
- Pattern looks like a filename: "*.js", "README.md", "test-*.tsx"
|
|
266
|
+
- User wants to find files by name/extension: "all TypeScript files", "Python scripts"
|
|
267
|
+
- Looking for configuration/setup files: ".env", "dockerfile", "tsconfig.json"
|
|
268
|
+
|
|
269
|
+
USE searchType="content" WHEN:
|
|
270
|
+
- User asks about code/logic: "authentication logic", "error handling", "API calls"
|
|
271
|
+
- Looking for functions/variables: "getUserData function", "useState hook"
|
|
272
|
+
- Searching for text/comments: "TODO items", "FIXME comments", "documentation"
|
|
273
|
+
- Finding patterns in code: "console.log statements", "import statements"
|
|
274
|
+
- User describes functionality: "components that handle login", "files with database queries"
|
|
275
|
+
|
|
276
|
+
WHEN UNSURE OR USER REQUEST IS AMBIGUOUS:
|
|
277
|
+
Run TWO searches in parallel - one for files and one for content:
|
|
278
|
+
|
|
279
|
+
Example approach for ambiguous queries like "find authentication stuff":
|
|
280
|
+
1. Start file search: searchType="files", pattern="auth"
|
|
281
|
+
2. Simultaneously start content search: searchType="content", pattern="authentication"
|
|
282
|
+
3. Present combined results: "Found 3 auth-related files and 8 files containing authentication code"
|
|
283
|
+
|
|
260
284
|
SEARCH TYPES:
|
|
261
285
|
- searchType="files": Find files by name (pattern matches file names)
|
|
262
286
|
- searchType="content": Search inside files for text patterns
|
|
263
287
|
|
|
288
|
+
PATTERN MATCHING MODES:
|
|
289
|
+
- Default (literalSearch=false): Patterns are treated as regular expressions
|
|
290
|
+
- Literal (literalSearch=true): Patterns are treated as exact strings
|
|
291
|
+
|
|
292
|
+
WHEN TO USE literalSearch=true:
|
|
293
|
+
Use literal search when searching for code patterns with special characters:
|
|
294
|
+
- Function calls with parentheses and quotes
|
|
295
|
+
- Array access with brackets
|
|
296
|
+
- Object methods with dots and parentheses
|
|
297
|
+
- File paths with backslashes
|
|
298
|
+
- Any pattern containing: . * + ? ^ $ { } [ ] | \\ ( )
|
|
299
|
+
|
|
264
300
|
IMPORTANT PARAMETERS:
|
|
265
301
|
- pattern: What to search for (file names OR content text)
|
|
302
|
+
- literalSearch: Use exact string matching instead of regex (default: false)
|
|
266
303
|
- filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json")
|
|
267
304
|
- ignoreCase: Case-insensitive search (default: true). Works for both file names and content.
|
|
268
305
|
- earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches)
|
|
269
306
|
|
|
270
|
-
EXAMPLES:
|
|
271
|
-
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
|
|
307
|
+
DECISION EXAMPLES:
|
|
308
|
+
- "find package.json" → searchType="files", pattern="package.json" (specific file)
|
|
309
|
+
- "find authentication components" → searchType="content", pattern="authentication" (looking for functionality)
|
|
310
|
+
- "locate all React components" → searchType="files", pattern="*.tsx" or "*.jsx" (file pattern)
|
|
311
|
+
- "find TODO comments" → searchType="content", pattern="TODO" (text in files)
|
|
312
|
+
- "show me login files" → AMBIGUOUS → run both: files with "login" AND content with "login"
|
|
313
|
+
- "find config" → AMBIGUOUS → run both: config files AND files containing config code
|
|
314
|
+
|
|
315
|
+
COMPREHENSIVE SEARCH EXAMPLES:
|
|
316
|
+
- Find package.json files: searchType="files", pattern="package.json"
|
|
317
|
+
- Find all JS files: searchType="files", pattern="*.js"
|
|
318
|
+
- Search for TODO in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
|
|
319
|
+
- Search for exact code: searchType="content", pattern="toast.error('test')", literalSearch=true
|
|
320
|
+
- Ambiguous request "find auth stuff": Run two searches:
|
|
321
|
+
1. searchType="files", pattern="auth"
|
|
322
|
+
2. searchType="content", pattern="authentication"
|
|
323
|
+
|
|
324
|
+
PRO TIP: When user requests are ambiguous about whether they want files or content,
|
|
325
|
+
run both searches concurrently and combine results for comprehensive coverage.
|
|
278
326
|
|
|
279
327
|
Unlike regular search tools, this starts a background search process and returns
|
|
280
328
|
immediately with a session ID. Use get_more_search_results to get results as they
|
|
@@ -635,6 +683,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
635
683
|
These IDs are for your reference only. Show users only the prompt titles and descriptions.
|
|
636
684
|
The IDs will be provided in the response metadata for your use.
|
|
637
685
|
|
|
686
|
+
DESKTOP COMMANDER INTRODUCTION: If a user asks "what is Desktop Commander?" or similar questions
|
|
687
|
+
about what Desktop Commander can do, answer that there are example use cases and tutorials
|
|
688
|
+
available, then call get_prompts with action='list_prompts' and category='onboarding' to show them.
|
|
689
|
+
|
|
638
690
|
ACTIONS:
|
|
639
691
|
- list_categories: Show all available prompt categories
|
|
640
692
|
- list_prompts: List prompts (optionally filtered by category)
|
|
@@ -673,6 +725,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
673
725
|
if (name === 'set_config_value' && args && typeof args === 'object' && 'key' in args) {
|
|
674
726
|
telemetryData.set_config_value_key_name = args.key;
|
|
675
727
|
}
|
|
728
|
+
if (name === 'get_prompts' && args && typeof args === 'object') {
|
|
729
|
+
const promptArgs = args;
|
|
730
|
+
telemetryData.action = promptArgs.action;
|
|
731
|
+
if (promptArgs.category) {
|
|
732
|
+
telemetryData.category = promptArgs.category;
|
|
733
|
+
telemetryData.has_category_filter = true;
|
|
734
|
+
}
|
|
735
|
+
if (promptArgs.promptId) {
|
|
736
|
+
telemetryData.prompt_id = promptArgs.promptId;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
676
739
|
capture_call_tool('server_call_tool', telemetryData);
|
|
677
740
|
// Track tool call
|
|
678
741
|
trackToolCall(name, args);
|
|
@@ -719,6 +782,70 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
719
782
|
case "get_prompts":
|
|
720
783
|
try {
|
|
721
784
|
result = await getPrompts(args || {});
|
|
785
|
+
// Capture detailed analytics for all successful get_prompts actions
|
|
786
|
+
if (args && typeof args === 'object' && !result.isError) {
|
|
787
|
+
const action = args.action;
|
|
788
|
+
try {
|
|
789
|
+
if (action === 'get_prompt' && args.promptId) {
|
|
790
|
+
// Existing get_prompt analytics
|
|
791
|
+
const { loadPromptsData } = await import('./tools/prompts.js');
|
|
792
|
+
const promptsData = await loadPromptsData();
|
|
793
|
+
const prompt = promptsData.prompts.find(p => p.id === args.promptId);
|
|
794
|
+
if (prompt) {
|
|
795
|
+
await capture('server_get_prompt', {
|
|
796
|
+
prompt_id: prompt.id,
|
|
797
|
+
prompt_title: prompt.title,
|
|
798
|
+
category: prompt.categories[0] || 'uncategorized',
|
|
799
|
+
author: prompt.author,
|
|
800
|
+
verified: prompt.verified
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else if (action === 'list_categories') {
|
|
805
|
+
// New analytics for category browsing
|
|
806
|
+
const { loadPromptsData } = await import('./tools/prompts.js');
|
|
807
|
+
const promptsData = await loadPromptsData();
|
|
808
|
+
// Extract unique categories and count prompts in each
|
|
809
|
+
const categoryMap = new Map();
|
|
810
|
+
promptsData.prompts.forEach(prompt => {
|
|
811
|
+
prompt.categories.forEach(category => {
|
|
812
|
+
categoryMap.set(category, (categoryMap.get(category) || 0) + 1);
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
await capture('server_list_prompt_categories', {
|
|
816
|
+
total_categories: categoryMap.size,
|
|
817
|
+
total_prompts: promptsData.prompts.length,
|
|
818
|
+
categories_available: Array.from(categoryMap.keys())
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
else if (action === 'list_prompts') {
|
|
822
|
+
// New analytics for prompt list browsing
|
|
823
|
+
const { loadPromptsData } = await import('./tools/prompts.js');
|
|
824
|
+
const promptsData = await loadPromptsData();
|
|
825
|
+
const category = args.category;
|
|
826
|
+
let filteredPrompts = promptsData.prompts;
|
|
827
|
+
if (category) {
|
|
828
|
+
filteredPrompts = promptsData.prompts.filter(prompt => prompt.categories.includes(category));
|
|
829
|
+
}
|
|
830
|
+
await capture('server_list_category_prompts', {
|
|
831
|
+
category_filter: category || 'all',
|
|
832
|
+
has_category_filter: !!category,
|
|
833
|
+
prompts_shown: filteredPrompts.length,
|
|
834
|
+
total_prompts_available: promptsData.prompts.length,
|
|
835
|
+
prompt_ids_shown: filteredPrompts.map(p => p.id)
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
// Don't fail the request if analytics fail
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Track if user used get_prompts after seeing onboarding invitation (for state management only)
|
|
844
|
+
const onboardingState = await usageTracker.getOnboardingState();
|
|
845
|
+
if (onboardingState.attemptsShown > 0 && !onboardingState.promptsUsed) {
|
|
846
|
+
// Mark that they used prompts after seeing onboarding (stops future onboarding messages)
|
|
847
|
+
await usageTracker.markOnboardingPromptsUsed();
|
|
848
|
+
}
|
|
722
849
|
}
|
|
723
850
|
catch (error) {
|
|
724
851
|
capture('server_request_error', { message: `Error in get_prompts handler: ${error}` });
|
|
@@ -816,6 +943,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
816
943
|
else {
|
|
817
944
|
await usageTracker.trackSuccess(name);
|
|
818
945
|
console.log(`[FEEDBACK DEBUG] Tool ${name} succeeded, checking feedback...`);
|
|
946
|
+
// Check if should show onboarding (before feedback - first-time users are priority)
|
|
947
|
+
const shouldShowOnboarding = await usageTracker.shouldShowOnboarding();
|
|
948
|
+
console.log(`[ONBOARDING DEBUG] Should show onboarding: ${shouldShowOnboarding}`);
|
|
949
|
+
if (shouldShowOnboarding) {
|
|
950
|
+
console.log(`[ONBOARDING DEBUG] Generating onboarding message...`);
|
|
951
|
+
const onboardingResult = await usageTracker.getOnboardingMessage();
|
|
952
|
+
console.log(`[ONBOARDING DEBUG] Generated variant: ${onboardingResult.variant}`);
|
|
953
|
+
// Capture onboarding prompt injection event
|
|
954
|
+
const stats = await usageTracker.getStats();
|
|
955
|
+
await capture('server_onboarding_shown', {
|
|
956
|
+
trigger_tool: name,
|
|
957
|
+
total_calls: stats.totalToolCalls,
|
|
958
|
+
successful_calls: stats.successfulCalls,
|
|
959
|
+
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
960
|
+
total_sessions: stats.totalSessions,
|
|
961
|
+
message_variant: onboardingResult.variant
|
|
962
|
+
});
|
|
963
|
+
// Inject onboarding message for the LLM
|
|
964
|
+
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
965
|
+
const currentContent = result.content[0].text || '';
|
|
966
|
+
result.content[0].text = `${currentContent}${onboardingResult.message}`;
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
result.content = [
|
|
970
|
+
...(result.content || []),
|
|
971
|
+
{
|
|
972
|
+
type: "text",
|
|
973
|
+
text: onboardingResult.message
|
|
974
|
+
}
|
|
975
|
+
];
|
|
976
|
+
}
|
|
977
|
+
// Mark that we've shown onboarding (to prevent spam)
|
|
978
|
+
await usageTracker.markOnboardingShown(onboardingResult.variant);
|
|
979
|
+
}
|
|
819
980
|
// Check if should prompt for feedback (only on successful operations)
|
|
820
981
|
const shouldPrompt = await usageTracker.shouldPromptForFeedback();
|
|
821
982
|
console.log(`[FEEDBACK DEBUG] Should prompt for feedback: ${shouldPrompt}`);
|
|
@@ -869,3 +1030,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
869
1030
|
};
|
|
870
1031
|
}
|
|
871
1032
|
});
|
|
1033
|
+
// Add no-op handlers so Visual Studio initialization succeeds
|
|
1034
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] }));
|
package/dist/tools/config.js
CHANGED
|
@@ -81,20 +81,22 @@ export async function setConfigValue(args) {
|
|
|
81
81
|
if ((parsed.data.key === 'allowedDirectories' || parsed.data.key === 'blockedCommands') &&
|
|
82
82
|
!Array.isArray(valueToStore)) {
|
|
83
83
|
if (typeof valueToStore === 'string') {
|
|
84
|
+
const originalString = valueToStore;
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
+
const parsedValue = JSON.parse(originalString);
|
|
87
|
+
valueToStore = parsedValue;
|
|
86
88
|
}
|
|
87
89
|
catch (parseError) {
|
|
88
90
|
console.error(`Failed to parse string as array for ${parsed.data.key}: ${parseError}`);
|
|
89
91
|
// If parsing failed and it's a single value, convert to an array with one item
|
|
90
|
-
if (!
|
|
91
|
-
valueToStore = [
|
|
92
|
+
if (!originalString.includes('[')) {
|
|
93
|
+
valueToStore = [originalString];
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
|
-
else {
|
|
96
|
-
// If not a string or array, convert to an array with one item
|
|
97
|
-
valueToStore = [valueToStore];
|
|
97
|
+
else if (valueToStore !== null) {
|
|
98
|
+
// If not a string or array (and not null), convert to an array with one item
|
|
99
|
+
valueToStore = [String(valueToStore)];
|
|
98
100
|
}
|
|
99
101
|
// Ensure the value is an array after all our conversions
|
|
100
102
|
if (!Array.isArray(valueToStore)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { ServerResult } from '../types.js';
|
|
2
|
+
interface Prompt {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
prompt: string;
|
|
7
|
+
categories: string[];
|
|
8
|
+
secondaryTag?: string;
|
|
9
|
+
votes: number;
|
|
10
|
+
gaClicks: number;
|
|
11
|
+
icon: string;
|
|
12
|
+
author: string;
|
|
13
|
+
verified: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PromptsData {
|
|
16
|
+
version: string;
|
|
17
|
+
description: string;
|
|
18
|
+
prompts: Prompt[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load prompts data from JSON file with caching
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadPromptsData(): Promise<PromptsData>;
|
|
2
24
|
/**
|
|
3
25
|
* Get prompts - main entry point for the tool
|
|
4
26
|
*/
|
|
5
27
|
export declare function getPrompts(params: any): Promise<ServerResult>;
|
|
28
|
+
export {};
|