Skip to content

Commit b5b338f

Browse files
committed
feat: MCP various Quality-of-Life UI/UX improvements
**Prompt to Tool Conversion:** - Convert MCP prompts (welcome, examples) to tools (qsv_welcome, qsv_examples) - Fix issue where prompts appeared as JSON file attachments in Claude Desktop - Tools now display text directly in chat interface - Add tool handlers in mcp-server.ts for qsv_welcome and qsv_examples - Create handleWelcomeTool() and handleExamplesTool() in mcp-tools.ts - Update manifest.json: set prompts_generated=false - Comment out deprecated registerPromptHandlers() method **qsv Binary Validation:** - Add auto-detection of qsv binary path using which/where commands - Implement version validation with minimum version check (13.0.0) - Add validateQsvBinary() and detectQsvBinaryPath() functions - Add startup logging for qsv binary validation status - Provide helpful error messages when qsv not found or version too old **Desktop Extension Config Fixes:** - Fix template variable expansion issues in manifest.json - Change working_dir to optional field (required=false) - Add detection of unexpanded ${user_config.*} template variables - Handle empty config fields gracefully with intelligent defaults - Treat unexpanded templates as empty values, falling back to ${DOWNLOADS} - Update getStringEnv() to detect literal "${user_config.*}" strings - Fix allowedDirs handling for unexpanded template variables **Icon Support:** - Add qsv-75x91.png icon to package - Fix manifest icons array to use correct MCPB v0.3 spec (size vs sizes) - Update package-mcpb.js to include icon file **Files Modified:** - src/mcp-server.ts - Add tool handlers, remove prompt handlers - src/mcp-tools.ts - Add welcome/examples tool definitions and handlers - src/config.ts - Add binary validation, fix template var handling - manifest.json - Update for tools, fix template variables, add icon - scripts/package-mcpb.js - Include icon in archive 🤖 Generated with Claude Code
1 parent 2672a74 commit b5b338f

File tree

10 files changed

+767
-261
lines changed

10 files changed

+767
-261
lines changed

.claude/skills/manifest.json

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"manifest_version": "0.3",
3-
"name": "qsv-data-wrangling",
3+
"name": "qsv",
44
"version": "13.0.0",
5-
"description": "Comprehensive data-wrangling toolkit for transforming, analyzing, querying & validating tabular data. Process local CSV, TSV, SSV, Excel/ODS, and JSONL files w/o uploading. Despite the generic warning, will only access APPROVED DIRECTORIES on your computer.",
5+
"description": "Blazingly fast Data Wrangling Toolkit for Tabular Data",
6+
"long_description": "Comprehensive data-wrangling toolkit for transforming, analyzing, querying & validating tabular data. Process local CSV, TSV, SSV, Excel/ODS, and JSONL files w/o uploading. Despite the warning, only accesses APPROVED DIRECTORIES on your computer.",
67
"author": {
78
"name": "datHere, Inc.",
89
"url": "https://dathere.com"
@@ -14,6 +15,12 @@
1415
"license": "MIT",
1516
"homepage": "https://github.com/dathere/qsv/tree/master/.claude/skills",
1617
"documentation": "https://github.com/dathere/qsv/blob/master/.claude/skills/README.md",
18+
"icons": [
19+
{
20+
"src": "qsv-75x91.png",
21+
"size": "75x91"
22+
}
23+
],
1724
"keywords": [
1825
"csv",
1926
"data-wrangling",
@@ -63,9 +70,8 @@
6370
"working_dir": {
6471
"type": "directory",
6572
"title": "Default Working Directory",
66-
"description": "Directory where qsv commands run by default. Extension only accesses files within this directory and allowed directories below.",
67-
"required": true,
68-
"default": "${DOWNLOADS}"
73+
"description": "Directory where qsv commands run by default. Leave empty to use Downloads folder automatically. Extension only accesses files within this directory and allowed directories below.",
74+
"required": false
6975
},
7076
"allowed_dirs": {
7177
"type": "directory",
@@ -137,18 +143,7 @@
137143
}
138144
],
139145
"tools_generated": true,
140-
"prompts": [
141-
{
142-
"name": "welcome",
143-
"description": "Welcome message and quick start guide for qsv",
144-
"arguments": []
145-
},
146-
{
147-
"name": "examples",
148-
"description": "Show common qsv usage examples",
149-
"arguments": []
150-
}
151-
],
146+
"prompts_generated": false,
152147
"compatibility": {
153148
"platforms": ["darwin", "win32", "linux"]
154149
},
@@ -162,7 +157,7 @@
162157
"Automatic Excel and JSONL conversion",
163158
"Filesystem resource browsing"
164159
],
165-
"minimum_qsv_version": "0.130.0"
160+
"minimum_qsv_version": "13.0.0"
166161
}
167162
}
168163
}

.claude/skills/qsv-75x91.png

7.7 KB
Loading

.claude/skills/qsv-mcp-server.mcpb

27.9 KB
Binary file not shown.

.claude/skills/scripts/package-mcpb.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,12 @@ async function createArchive() {
155155
archive.directory(join(rootDir, 'qsv'), 'qsv');
156156

157157
// Add icon if it exists
158-
const iconPath = join(rootDir, 'icon.png');
158+
const iconPath = join(rootDir, 'qsv-75x91.png');
159159
if (existsSync(iconPath)) {
160-
console.log(' Adding icon.png...');
161-
archive.file(iconPath, { name: 'icon.png' });
160+
console.log(' Adding qsv-75x91.png...');
161+
archive.file(iconPath, { name: 'qsv-75x91.png' });
162162
} else {
163-
console.log(' ℹ️ No icon.png found (optional)');
163+
console.log(' ℹ️ No qsv-75x91.png found (optional)');
164164
}
165165

166166
// Finalize archive

.claude/skills/src/config.ts

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { homedir, tmpdir } from 'os';
99
import { join } from 'path';
10+
import { execSync } from 'child_process';
1011

1112
/**
1213
* Expand template variables in strings
@@ -99,9 +100,15 @@ function parseFloatEnv(envVar: string, defaultValue: number, min?: number, max?:
99100
* Get string from environment variable with default
100101
* Expands template variables like ${HOME}, ${USERPROFILE}, etc.
101102
* Uses nullish coalescing (??) to allow empty strings while falling back for undefined/null
103+
* Also treats empty strings and unexpanded template vars as missing values for user convenience
102104
*/
103105
function getStringEnv(envVar: string, defaultValue: string): string {
104-
const value = process.env[envVar] ?? defaultValue;
106+
const value = process.env[envVar];
107+
// Treat empty string, null, undefined, or unexpanded template as missing - use default
108+
// Unexpanded template happens when Claude Desktop config field is empty
109+
if (!value || value.trim() === '' || value.includes('${user_config.')) {
110+
return expandTemplateVars(defaultValue);
111+
}
105112
return expandTemplateVars(value);
106113
}
107114

@@ -140,6 +147,139 @@ function getBooleanEnv(envVar: string, defaultValue: boolean): boolean {
140147
return defaultValue;
141148
}
142149

150+
/**
151+
* Minimum required qsv version
152+
* Set to 13.0.0 to minimize support issues and encourage users to update
153+
*/
154+
export const MINIMUM_QSV_VERSION = '13.0.0';
155+
156+
/**
157+
* Validation result interface
158+
*/
159+
export interface QsvValidationResult {
160+
valid: boolean;
161+
version?: string;
162+
path?: string;
163+
error?: string;
164+
}
165+
166+
/**
167+
* Auto-detect absolute path to qsv binary
168+
* Uses 'which' on Unix/macOS or 'where' on Windows
169+
*/
170+
function detectQsvBinaryPath(): string | null {
171+
try {
172+
const command = process.platform === 'win32' ? 'where qsv' : 'which qsv';
173+
const result = execSync(command, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
174+
const path = result.trim().split('\n')[0]; // Take first result
175+
return path || null;
176+
} catch {
177+
return null;
178+
}
179+
}
180+
181+
/**
182+
* Parse version string from qsv --version output
183+
* Example: "qsv 0.135.0" -> "0.135.0"
184+
*/
185+
function parseQsvVersion(versionOutput: string): string | null {
186+
const match = versionOutput.match(/qsv\s+(\d+\.\d+\.\d+)/);
187+
return match ? match[1] : null;
188+
}
189+
190+
/**
191+
* Compare two semantic version strings
192+
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
193+
*/
194+
function compareVersions(v1: string, v2: string): number {
195+
const parts1 = v1.split('.').map(Number);
196+
const parts2 = v2.split('.').map(Number);
197+
198+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
199+
const part1 = parts1[i] || 0;
200+
const part2 = parts2[i] || 0;
201+
if (part1 < part2) return -1;
202+
if (part1 > part2) return 1;
203+
}
204+
return 0;
205+
}
206+
207+
/**
208+
* Validate qsv binary at given path
209+
* Runs 'qsv --version' to check if binary exists and meets minimum version
210+
*/
211+
export function validateQsvBinary(binPath: string): QsvValidationResult {
212+
try {
213+
const result = execSync(`"${binPath}" --version`, {
214+
encoding: 'utf8',
215+
stdio: ['ignore', 'pipe', 'pipe'],
216+
timeout: 5000 // 5 second timeout
217+
});
218+
219+
const version = parseQsvVersion(result);
220+
if (!version) {
221+
return {
222+
valid: false,
223+
error: `Could not parse version from qsv output: ${result.trim()}`,
224+
};
225+
}
226+
227+
if (compareVersions(version, MINIMUM_QSV_VERSION) < 0) {
228+
return {
229+
valid: false,
230+
version,
231+
path: binPath,
232+
error: `qsv version ${version} found, but ${MINIMUM_QSV_VERSION} or higher is required`,
233+
};
234+
}
235+
236+
return {
237+
valid: true,
238+
version,
239+
path: binPath,
240+
};
241+
} catch (error) {
242+
const errorMessage = error instanceof Error ? error.message : String(error);
243+
return {
244+
valid: false,
245+
error: `Failed to execute qsv binary at "${binPath}": ${errorMessage}`,
246+
};
247+
}
248+
}
249+
250+
/**
251+
* Initialize qsv binary path with auto-detection and validation
252+
* Priority:
253+
* 1. Explicit QSV_MCP_BIN_PATH environment variable
254+
* 2. Auto-detected path from system PATH
255+
* 3. Fall back to 'qsv' (will likely fail validation if not in PATH)
256+
*/
257+
function initializeQsvBinaryPath(): { path: string; validation: QsvValidationResult } {
258+
const explicitPath = process.env['QSV_MCP_BIN_PATH'];
259+
260+
// If user explicitly configured a path, use it
261+
if (explicitPath) {
262+
const expanded = expandTemplateVars(explicitPath);
263+
const validation = validateQsvBinary(expanded);
264+
return { path: expanded, validation };
265+
}
266+
267+
// Try to auto-detect from PATH
268+
const detectedPath = detectQsvBinaryPath();
269+
if (detectedPath) {
270+
const validation = validateQsvBinary(detectedPath);
271+
if (validation.valid) {
272+
return { path: detectedPath, validation };
273+
}
274+
// Detected but invalid - fall through to default
275+
}
276+
277+
// Fall back to 'qsv' (will work if in PATH, otherwise will fail)
278+
const fallbackPath = 'qsv';
279+
const validation = validateQsvBinary(fallbackPath);
280+
return { path: fallbackPath, validation };
281+
}
282+
143283
/**
144284
* Detect if running in Desktop Extension mode
145285
* Desktop extensions set MCPB_EXTENSION_MODE=true
@@ -148,21 +288,32 @@ export function isExtensionMode(): boolean {
148288
return getBooleanEnv('MCPB_EXTENSION_MODE', false);
149289
}
150290

291+
/**
292+
* Initialize qsv binary path with auto-detection
293+
*/
294+
const qsvBinaryInit = initializeQsvBinaryPath();
295+
151296
/**
152297
* Configuration object with all configurable settings
153298
*/
154299
export const config = {
155300
/**
156301
* Path to qsv binary
157-
* Default: 'qsv' (assumes qsv is in PATH)
302+
* Auto-detected from PATH if not explicitly configured
303+
*/
304+
qsvBinPath: qsvBinaryInit.path,
305+
306+
/**
307+
* Validation result for qsv binary
308+
* Contains version info and any errors
158309
*/
159-
qsvBinPath: getStringEnv('QSV_MCP_BIN_PATH', 'qsv'),
310+
qsvValidation: qsvBinaryInit.validation,
160311

161312
/**
162313
* Working directory for relative paths
163314
* Default: Current working directory
164315
*/
165-
workingDir: getStringEnv('QSV_MCP_WORKING_DIR', process.cwd()),
316+
workingDir: getStringEnv('QSV_MCP_WORKING_DIR', '${DOWNLOADS}'),
166317

167318
/**
168319
* Allowed directories for file access
@@ -173,7 +324,10 @@ export const config = {
173324
*/
174325
allowedDirs: (() => {
175326
const envValue = process.env['QSV_MCP_ALLOWED_DIRS'];
176-
if (!envValue) return [];
327+
// Treat empty, undefined, or unexpanded template as empty array
328+
if (!envValue || envValue.trim() === '' || envValue.includes('${user_config.')) {
329+
return [];
330+
}
177331

178332
// Try parsing as JSON array first (Desktop extension mode)
179333
try {

0 commit comments

Comments
 (0)