@tigerdata/mcp-boilerplate 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills/tool.js +48 -10
- package/dist/skills/tool.spec.d.ts +1 -0
- package/dist/skills/tool.spec.js +86 -0
- package/dist/skills/utils.d.ts +22 -0
- package/dist/skills/utils.js +61 -13
- package/dist/skills/utils.spec.d.ts +1 -0
- package/dist/skills/utils.spec.js +110 -0
- package/package.json +3 -2
package/dist/skills/tool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
1
2
|
import { zViewSkillInputSchema, zViewSkillOutputSchema, } from './types.js';
|
|
2
|
-
import { listSkills, parseSkillsFlags, skillsDescription, viewSkillContent, } from './utils.js';
|
|
3
|
+
import { getAvailableSkillNames, InvalidPathError, listSkills, PathNotFoundError, parseSkillsFlags, SkillNotFoundError, SkillsApiError, skillsDescription, viewSkillContent, } from './utils.js';
|
|
3
4
|
export const createViewSkillToolFactory = (options = {}) => async (ctx, mcpFlags) => {
|
|
4
5
|
const { octokit } = ctx;
|
|
5
6
|
const flags = parseSkillsFlags(mcpFlags.query);
|
|
@@ -20,7 +21,6 @@ export const createViewSkillToolFactory = (options = {}) => async (ctx, mcpFlags
|
|
|
20
21
|
annotations: {
|
|
21
22
|
readOnlyHint: true,
|
|
22
23
|
idempotentHint: true,
|
|
23
|
-
openWorldHint: true,
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
fn: async ({ skill_name: name, path, }) => {
|
|
@@ -29,14 +29,52 @@ export const createViewSkillToolFactory = (options = {}) => async (ctx, mcpFlags
|
|
|
29
29
|
content: await listSkills({ octokit, flags }),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
content: await viewSkillContent({
|
|
35
|
+
octokit,
|
|
36
|
+
flags,
|
|
37
|
+
name,
|
|
38
|
+
path,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (!(err instanceof SkillsApiError)) {
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
if (err instanceof SkillNotFoundError) {
|
|
47
|
+
log.warn(`Skill not found: ${err.skillName}`, {
|
|
48
|
+
error: err.constructor.name,
|
|
49
|
+
skill: err.skillName,
|
|
50
|
+
});
|
|
51
|
+
const available = await getAvailableSkillNames({ octokit, flags });
|
|
52
|
+
return {
|
|
53
|
+
content: `Skill not found: ${err.skillName}. Available skills: ${available}. Use one of these names.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (err instanceof PathNotFoundError) {
|
|
57
|
+
log.warn(`Path not found: ${err.path} in skill ${err.skill}`, {
|
|
58
|
+
error: err.constructor.name,
|
|
59
|
+
skill: err.skill,
|
|
60
|
+
path: err.path,
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
content: `Path not found: ${err.path}. Contents of skill "${err.skill}":\n${err.listing}\n\nUse path "SKILL.md" to read the main skill document.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (err instanceof InvalidPathError) {
|
|
67
|
+
log.warn(`Invalid path: ${err.path}`, {
|
|
68
|
+
error: err.constructor.name,
|
|
69
|
+
path: err.path,
|
|
70
|
+
});
|
|
71
|
+
const available = await getAvailableSkillNames({ octokit, flags });
|
|
72
|
+
return {
|
|
73
|
+
content: `${err.message} Available skills: ${available}. Use name "." to list skills; use path "." to list a skill's contents.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
40
78
|
},
|
|
41
79
|
};
|
|
42
80
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, spyOn, } from 'bun:test';
|
|
2
|
+
import Path from 'node:path';
|
|
3
|
+
import { log } from '../logger.js';
|
|
4
|
+
import { createViewSkillToolFactory } from './tool.js';
|
|
5
|
+
process.env.SKILLS_FILE = Path.resolve(import.meta.dir, '__fixtures__', 'skills.yaml');
|
|
6
|
+
describe('createViewSkillToolFactory', () => {
|
|
7
|
+
let warnSpy;
|
|
8
|
+
let fn;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
const factory = createViewSkillToolFactory();
|
|
11
|
+
const tool = await factory({ octokit: null }, { query: {} });
|
|
12
|
+
fn = tool.fn;
|
|
13
|
+
warnSpy = spyOn(log, 'warn');
|
|
14
|
+
});
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
warnSpy.mockClear();
|
|
17
|
+
});
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
warnSpy.mockRestore();
|
|
20
|
+
});
|
|
21
|
+
it('lists skills when skill_name is "."', async () => {
|
|
22
|
+
const result = await fn({ skill_name: '.', path: '' });
|
|
23
|
+
expect(result.content).toContain('<available_skills>');
|
|
24
|
+
expect(result.content).toContain('first-skill');
|
|
25
|
+
expect(result.content).toContain('second-skill');
|
|
26
|
+
});
|
|
27
|
+
it('lists skills when skill_name is empty', async () => {
|
|
28
|
+
const result = await fn({ skill_name: '', path: '' });
|
|
29
|
+
expect(result.content).toContain('<available_skills>');
|
|
30
|
+
expect(result.content).toContain('first-skill');
|
|
31
|
+
expect(result.content).toContain('second-skill');
|
|
32
|
+
});
|
|
33
|
+
it('returns skill content for valid name and path', async () => {
|
|
34
|
+
const result = await fn({ skill_name: 'first-skill', path: 'SKILL.md' });
|
|
35
|
+
expect(result.content).toBe('First skill content\n');
|
|
36
|
+
});
|
|
37
|
+
it('returns recovery string with available skills for nonexistent skill', async () => {
|
|
38
|
+
const result = await fn({ skill_name: 'nonexistent', path: 'SKILL.md' });
|
|
39
|
+
expect(result.content).toContain('Skill not found: nonexistent');
|
|
40
|
+
expect(result.content).toContain('first-skill');
|
|
41
|
+
expect(result.content).toContain('second-skill');
|
|
42
|
+
expect(warnSpy).toHaveBeenCalledWith('Skill not found: nonexistent', expect.objectContaining({
|
|
43
|
+
error: 'SkillNotFoundError',
|
|
44
|
+
skill: 'nonexistent',
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
it('returns recovery string with directory listing for nonexistent path', async () => {
|
|
48
|
+
const result = await fn({
|
|
49
|
+
skill_name: 'first-skill',
|
|
50
|
+
path: 'does-not-exist',
|
|
51
|
+
});
|
|
52
|
+
expect(result.content).toContain('Path not found: does-not-exist');
|
|
53
|
+
expect(result.content).toContain('first-skill');
|
|
54
|
+
expect(result.content).toContain('SKILL.md');
|
|
55
|
+
expect(warnSpy).toHaveBeenCalledWith('Path not found: does-not-exist in skill first-skill', expect.objectContaining({
|
|
56
|
+
error: 'PathNotFoundError',
|
|
57
|
+
skill: 'first-skill',
|
|
58
|
+
path: 'does-not-exist',
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
it('returns recovery string for directory traversal path', async () => {
|
|
62
|
+
const result = await fn({
|
|
63
|
+
skill_name: 'first-skill',
|
|
64
|
+
path: '../../etc/passwd',
|
|
65
|
+
});
|
|
66
|
+
expect(result.content).toContain('Invalid path: ../../etc/passwd.');
|
|
67
|
+
expect(result.content).toContain('first-skill');
|
|
68
|
+
expect(result.content).toContain('second-skill');
|
|
69
|
+
expect(warnSpy).toHaveBeenCalledWith('Invalid path: ../../etc/passwd', expect.objectContaining({
|
|
70
|
+
error: 'InvalidPathError',
|
|
71
|
+
path: '../../etc/passwd',
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
it('returns recovery string for null byte path', async () => {
|
|
75
|
+
const result = await fn({
|
|
76
|
+
skill_name: 'first-skill',
|
|
77
|
+
path: 'SKILL.md\x00.txt',
|
|
78
|
+
});
|
|
79
|
+
expect(result.content).toContain('Invalid path: SKILL.md\x00.txt.');
|
|
80
|
+
expect(result.content).toContain('first-skill');
|
|
81
|
+
expect(warnSpy).toHaveBeenCalledWith('Invalid path: SKILL.md\x00.txt', expect.objectContaining({
|
|
82
|
+
error: 'InvalidPathError',
|
|
83
|
+
path: 'SKILL.md\x00.txt',
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
});
|
package/dist/skills/utils.d.ts
CHANGED
|
@@ -17,6 +17,28 @@ export declare const resolveSkill: ({ name, octokit, flags, force, }: {
|
|
|
17
17
|
force?: boolean;
|
|
18
18
|
}) => Promise<Skill | null>;
|
|
19
19
|
export declare const skillVisible: (name: string, flags: SkillsFlags) => boolean;
|
|
20
|
+
/** Base class for skills API errors; catch in tool.ts to format recovery messages. */
|
|
21
|
+
export declare class SkillsApiError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export declare class SkillNotFoundError extends SkillsApiError {
|
|
24
|
+
readonly skillName: string;
|
|
25
|
+
constructor(skillName: string);
|
|
26
|
+
}
|
|
27
|
+
export declare class PathNotFoundError extends SkillsApiError {
|
|
28
|
+
readonly skill: string;
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly listing: string;
|
|
31
|
+
constructor(skill: string, path: string, listing: string);
|
|
32
|
+
}
|
|
33
|
+
export declare class InvalidPathError extends SkillsApiError {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
constructor(path: string);
|
|
36
|
+
}
|
|
37
|
+
/** Comma-separated visible skill names for error/recovery messages. */
|
|
38
|
+
export declare const getAvailableSkillNames: ({ octokit, flags, }: {
|
|
39
|
+
octokit?: Octokit | null;
|
|
40
|
+
flags?: SkillsFlags;
|
|
41
|
+
}) => Promise<string>;
|
|
20
42
|
export declare const listSkills: ({ octokit, flags, force, }?: {
|
|
21
43
|
octokit?: Octokit | null;
|
|
22
44
|
flags?: SkillsFlags;
|
package/dist/skills/utils.js
CHANGED
|
@@ -251,6 +251,48 @@ export const skillVisible = (name, flags) => {
|
|
|
251
251
|
}
|
|
252
252
|
return true;
|
|
253
253
|
};
|
|
254
|
+
/** Base class for skills API errors; catch in tool.ts to format recovery messages. */
|
|
255
|
+
export class SkillsApiError extends Error {
|
|
256
|
+
}
|
|
257
|
+
export class SkillNotFoundError extends SkillsApiError {
|
|
258
|
+
skillName;
|
|
259
|
+
constructor(skillName) {
|
|
260
|
+
super(`Skill not found: ${skillName}.`);
|
|
261
|
+
this.skillName = skillName;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export class PathNotFoundError extends SkillsApiError {
|
|
265
|
+
skill;
|
|
266
|
+
path;
|
|
267
|
+
listing;
|
|
268
|
+
constructor(skill, path, listing) {
|
|
269
|
+
super(`Path not found: ${path}.`);
|
|
270
|
+
this.skill = skill;
|
|
271
|
+
this.path = path;
|
|
272
|
+
this.listing = listing;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export class InvalidPathError extends SkillsApiError {
|
|
276
|
+
path;
|
|
277
|
+
constructor(path) {
|
|
278
|
+
super(`Invalid path: ${path}.`);
|
|
279
|
+
this.path = path;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Comma-separated visible skill names for error/recovery messages. */
|
|
283
|
+
export const getAvailableSkillNames = async ({ octokit, flags = {}, }) => {
|
|
284
|
+
try {
|
|
285
|
+
const skills = await loadSkills({ octokit, force: false });
|
|
286
|
+
const names = [...skills.values()]
|
|
287
|
+
.filter((s) => skillVisible(s.name, flags))
|
|
288
|
+
.map((s) => s.name)
|
|
289
|
+
.sort();
|
|
290
|
+
return names.length > 0 ? names.join(', ') : '(none)';
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
throw new Error(`getAvailableSkillNames failed: ${err.message}`);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
254
296
|
export const listSkills = async ({ octokit, flags = {}, force = false, } = {}) => {
|
|
255
297
|
const skills = await loadSkills({ octokit, force });
|
|
256
298
|
return `<available_skills>
|
|
@@ -265,7 +307,7 @@ ${encode([...skills.values()]
|
|
|
265
307
|
export const viewSkillContent = async ({ octokit, flags = {}, name, path: passedPath, }) => {
|
|
266
308
|
const skill = await resolveSkill({ octokit, flags, name });
|
|
267
309
|
if (!skill) {
|
|
268
|
-
throw new
|
|
310
|
+
throw new SkillNotFoundError(name);
|
|
269
311
|
}
|
|
270
312
|
const targetPath = passedPath || 'SKILL.md';
|
|
271
313
|
const cacheKey = `${name}/${normalizeSkillPath(targetPath)}`;
|
|
@@ -286,7 +328,7 @@ const normalizeSkillPath = (path) => {
|
|
|
286
328
|
.replace(/^(\.?\/)+/, '');
|
|
287
329
|
if (normalizedPath.split('/').some((s) => s === '..') ||
|
|
288
330
|
normalizedPath.includes('\0')) {
|
|
289
|
-
throw new
|
|
331
|
+
throw new InvalidPathError(path);
|
|
290
332
|
}
|
|
291
333
|
return normalizedPath;
|
|
292
334
|
};
|
|
@@ -297,12 +339,20 @@ const getSkillContent = async ({ skill, path: targetPath, octokit, }) => {
|
|
|
297
339
|
const root = Path.resolve(skill.path);
|
|
298
340
|
const target = Path.resolve(Path.join(root, normalizedPath));
|
|
299
341
|
if (targetPath !== '.' && !target.startsWith(root)) {
|
|
300
|
-
throw new
|
|
342
|
+
throw new InvalidPathError(targetPath);
|
|
301
343
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
344
|
+
let stats;
|
|
345
|
+
try {
|
|
346
|
+
stats = await stat(target);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
350
|
+
const listing = entries
|
|
351
|
+
.map((entry) => `${entry.isDirectory() ? '📁' : '📄'} ${entry.name}`)
|
|
352
|
+
.join('\n');
|
|
353
|
+
throw new PathNotFoundError(skill.name, targetPath, listing || '(empty)');
|
|
354
|
+
}
|
|
355
|
+
if (stats.isDirectory()) {
|
|
306
356
|
const entries = await readdir(target, {
|
|
307
357
|
withFileTypes: true,
|
|
308
358
|
});
|
|
@@ -313,7 +363,7 @@ const getSkillContent = async ({ skill, path: targetPath, octokit, }) => {
|
|
|
313
363
|
.join('\n');
|
|
314
364
|
return `Directory listing for ${skill.name}/${normalizedPath}:\n${listing}`;
|
|
315
365
|
}
|
|
316
|
-
else if (
|
|
366
|
+
else if (stats.isFile()) {
|
|
317
367
|
return await readFile(target, 'utf-8');
|
|
318
368
|
}
|
|
319
369
|
else {
|
|
@@ -363,11 +413,9 @@ This tool provides access to domain-specific skills - structured knowledge and p
|
|
|
363
413
|
|
|
364
414
|
## How to Use Skills
|
|
365
415
|
|
|
366
|
-
1. **Discover**: If you have not been provided the list of skills, fetch them by invoking this tool with \`name: "."
|
|
367
|
-
2. **Read**: Access a skill by reading its SKILL.md file: \`name: "skill-name", path: "SKILL.md"
|
|
368
|
-
3. **Explore**: Navigate within the skill directory to find additional resources, examples, or templates.
|
|
369
|
-
The SKILL.md file and other documents may contain relative links to guide you.
|
|
370
|
-
You can list the content of directories by specifying the directory path, relative to the skill root.
|
|
416
|
+
1. **Discover**: If you have not been provided the list of skills, fetch them by invoking this tool with \`name: "."\`. Use only skill names that appear in that list.
|
|
417
|
+
2. **Read**: Access a skill by reading its SKILL.md file: \`name: "skill-name", path: "SKILL.md"\`. Use a skill name from the list (step 1); do not guess names from context or topic words.
|
|
418
|
+
3. **Explore**: Navigate within the skill directory to find additional resources, examples, or templates. You can list the contents of the directory by using \`path: "."\` (relative to the skill root); use only a path that appears in that listing—do not guess or infer path names from skill descriptions or topic words (e.g. "indexing strategies" in text is not a path). SKILL.md and other documents may contain relative links to guide you.
|
|
371
419
|
4. **Apply**: Follow the procedures and reference the knowledge in the skill to complete your task
|
|
372
420
|
|
|
373
421
|
## Skill Structure
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import Path from 'node:path';
|
|
3
|
+
import { getAvailableSkillNames, InvalidPathError, listSkills, PathNotFoundError, SkillNotFoundError, viewSkillContent, } from './utils.js';
|
|
4
|
+
process.env.SKILLS_FILE = Path.resolve(import.meta.dir, '__fixtures__', 'skills.yaml');
|
|
5
|
+
describe('Skills API', () => {
|
|
6
|
+
describe('getAvailableSkillNames', () => {
|
|
7
|
+
it('returns a comma-separated list of visible skill names in alphabetical order when skills are loaded', async () => {
|
|
8
|
+
const names = await getAvailableSkillNames({});
|
|
9
|
+
expect(names).toBe('first-skill, second-skill');
|
|
10
|
+
});
|
|
11
|
+
it('returns "(none)" when flags filter out every skill (e.g. enabledSkills does not match any)', async () => {
|
|
12
|
+
const names = await getAvailableSkillNames({
|
|
13
|
+
flags: {
|
|
14
|
+
enabledSkills: new Set(['does-not-exist']),
|
|
15
|
+
disabledSkills: null,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
expect(names).toBe('(none)');
|
|
19
|
+
});
|
|
20
|
+
it('throws an error with message prefixed by "getAvailableSkillNames failed:" when something in the function fails', async () => {
|
|
21
|
+
await expect(getAvailableSkillNames({
|
|
22
|
+
flags: {
|
|
23
|
+
enabledSkills: {
|
|
24
|
+
has: () => {
|
|
25
|
+
throw new Error('enabledSkills filter threw');
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
disabledSkills: null,
|
|
29
|
+
},
|
|
30
|
+
})).rejects.toThrow('getAvailableSkillNames failed: enabledSkills filter threw');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('listSkills and viewSkillContent (skills-api-test-plan)', () => {
|
|
34
|
+
it('list all skills: returns a string containing available_skills and all loaded skill names', async () => {
|
|
35
|
+
const result = await listSkills({});
|
|
36
|
+
expect(result).toContain('<available_skills>');
|
|
37
|
+
expect(result).toContain('first-skill');
|
|
38
|
+
expect(result).toContain('second-skill');
|
|
39
|
+
const inner = result
|
|
40
|
+
.split('<available_skills>')[1]
|
|
41
|
+
.split('</available_skills>')[0];
|
|
42
|
+
const lines = inner.trim().split('\n');
|
|
43
|
+
expect(lines).toHaveLength(3);
|
|
44
|
+
});
|
|
45
|
+
it('read valid skill: returns the content of the skill SKILL.md when name and path are valid', async () => {
|
|
46
|
+
const result = await viewSkillContent({
|
|
47
|
+
name: 'first-skill',
|
|
48
|
+
path: 'SKILL.md',
|
|
49
|
+
});
|
|
50
|
+
expect(result).toBe('First skill content\n');
|
|
51
|
+
});
|
|
52
|
+
it('skill not found: viewSkillContent throws SkillNotFoundError', async () => {
|
|
53
|
+
try {
|
|
54
|
+
await viewSkillContent({ name: 'nonexistent-skill', path: 'SKILL.md' });
|
|
55
|
+
throw new Error('Expected SkillNotFoundError to be thrown');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
expect(err).toBeInstanceOf(SkillNotFoundError);
|
|
59
|
+
expect(err.skillName).toBe('nonexistent-skill');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it('skill not found with another name: throws SkillNotFoundError', async () => {
|
|
63
|
+
try {
|
|
64
|
+
await viewSkillContent({
|
|
65
|
+
name: 'another-missing-skill',
|
|
66
|
+
path: 'SKILL.md',
|
|
67
|
+
});
|
|
68
|
+
throw new Error('Expected SkillNotFoundError to be thrown');
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
expect(err).toBeInstanceOf(SkillNotFoundError);
|
|
72
|
+
expect(err.skillName).toBe('another-missing-skill');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it('path not found inside valid skill: viewSkillContent throws PathNotFoundError', async () => {
|
|
76
|
+
try {
|
|
77
|
+
await viewSkillContent({
|
|
78
|
+
name: 'first-skill',
|
|
79
|
+
path: 'indexing-strategies',
|
|
80
|
+
});
|
|
81
|
+
throw new Error('Expected PathNotFoundError to be thrown');
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
expect(err).toBeInstanceOf(PathNotFoundError);
|
|
85
|
+
expect(err.skill).toBe('first-skill');
|
|
86
|
+
expect(err.path).toBe('indexing-strategies');
|
|
87
|
+
expect(err.listing).toContain('SKILL.md');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it('invalid path directory traversal: viewSkillContent throws InvalidPathError', async () => {
|
|
91
|
+
try {
|
|
92
|
+
await viewSkillContent({
|
|
93
|
+
name: 'first-skill',
|
|
94
|
+
path: '../../etc/passwd',
|
|
95
|
+
});
|
|
96
|
+
throw new Error('Expected InvalidPathError to be thrown');
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
expect(err).toBeInstanceOf(InvalidPathError);
|
|
100
|
+
expect(err.path).toBe('../../etc/passwd');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it('invalid path with null byte: viewSkillContent throws InvalidPathError', async () => {
|
|
104
|
+
await expect(viewSkillContent({
|
|
105
|
+
name: 'first-skill',
|
|
106
|
+
path: 'SKILL.md\x00.txt',
|
|
107
|
+
})).rejects.toThrow(InvalidPathError);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tigerdata/mcp-boilerplate",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "MCP boilerplate code for Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "TigerData",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"build": "tsc",
|
|
38
38
|
"prepublishOnly": "tsc",
|
|
39
39
|
"watch": "tsc --watch",
|
|
40
|
-
"lint": "./bun x @biomejs/biome check"
|
|
40
|
+
"lint": "./bun x @biomejs/biome check",
|
|
41
|
+
"test": "./bun test ./src"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@mcp-use/inspector": "^0.24.5",
|