/**
* Test that skills methods work with both keyword and positional arguments
*
* In MCP protocol and Claude Code execution:
* - Keyword: await skills.find({ query: "pdf", depth: 2 })
* - Positional: await skills.find("pdf", 2)
*
* Both should work thanks to the inputSchema parameter ordering
*/
import { SkillsManagementMCP } from '../../src/internal-mcps/skills.js';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs/promises';
import { setOverrideWorkingDirectory, resetPathsCache } from '../../src/utils/ncp-paths.js';
describe('Skills Methods - Positional Arguments Support', () => {
let skillsMCP: SkillsManagementMCP;
let testWorkingDir: string;
let stubbedSkills: any[];
beforeEach(async () => {
testWorkingDir = path.join(os.tmpdir(), `ncp-skills-mcp-${Date.now()}`);
await fs.mkdir(testWorkingDir, { recursive: true });
stubbedSkills = [];
resetPathsCache();
setOverrideWorkingDirectory(testWorkingDir);
skillsMCP = new SkillsManagementMCP();
// Stub heavy dependencies so tests don't hit real filesystem/network
(skillsMCP as any).skillsManager = {
initialize: async () => {},
loadAllSkills: async () => stubbedSkills,
getLoadedSkills: () => stubbedSkills
};
(skillsMCP as any).marketplaceClient = {
initialize: async () => {},
install: async () => ({ success: false, message: 'Skill not found' }),
remove: async () => ({ success: true, message: 'Removed skill' })
};
});
afterEach(async () => {
resetPathsCache();
setOverrideWorkingDirectory(null);
await fs.rm(testWorkingDir, { recursive: true, force: true });
});
describe('skills:find parameter handling', () => {
it('should accept keyword arguments', async () => {
// Keyword arguments format
const result = await skillsMCP.executeTool('find', {
query: 'canvas',
depth: 1
});
expect(result.success).toBe(true);
// Should not error when using keyword arguments
expect(result.content).toBeDefined();
});
it('should support empty call (list all)', async () => {
// No arguments - should list all
const result = await skillsMCP.executeTool('find', {});
expect(result.success).toBe(true);
expect(result.content).toBeDefined();
});
it('should handle missing optional parameters', async () => {
// Only query, other params should use defaults
const result = await skillsMCP.executeTool('find', {
query: 'design'
});
expect(result.success).toBe(true);
// depth should default to 1, page to 1, limit to 10
expect(result.content).toBeDefined();
});
it('should apply defaults correctly', async () => {
// Empty params should use all defaults
const result = await skillsMCP.executeTool('find', {});
expect(result.success).toBe(true);
// Should work with all defaults: query='', depth=1, page=1, limit=10
expect(result.content).toBeDefined();
});
it('should respect pagination parameters', async () => {
const result = await skillsMCP.executeTool('find', {
page: 2,
limit: 5
});
expect(result.success).toBe(true);
expect(result.content).toBeDefined();
});
});
describe('skills:add parameter handling', () => {
it('should handle required parameter', async () => {
const result = await skillsMCP.executeTool('add', {
skill_name: 'nonexistent-skill'
});
// Will fail because skill doesn't exist, but parameter parsing should work
expect(result.content).toBeDefined();
// Should fail with "not found" message, not "missing parameter" message
expect(result.content).not.toContain('Missing required');
});
it('should require skill_name parameter', async () => {
const result = await skillsMCP.executeTool('add', {});
expect(result.success).toBe(false);
expect(result.content).toContain('Missing required');
});
});
describe('skills:remove parameter handling', () => {
it('should handle required parameter', async () => {
const result = await skillsMCP.executeTool('remove', {
skill_name: 'nonexistent'
});
expect(result.content).toBeDefined();
});
it('should require skill_name parameter', async () => {
const result = await skillsMCP.executeTool('remove', {});
expect(result.success).toBe(false);
expect(result.content).toContain('Missing required');
});
});
describe('skills:read_resource parameter handling', () => {
it('should handle required parameters', async () => {
const result = await skillsMCP.executeTool('read_resource', {
skill_name: 'nonexistent',
file_path: 'resources/test.md'
});
expect(result.content).toBeDefined();
// Should fail with "skill not found", not "missing parameter"
expect(result.content).not.toContain('Missing required');
});
it('should require both parameters', async () => {
const result1 = await skillsMCP.executeTool('read_resource', {
skill_name: 'test'
});
expect(result1.success).toBe(false);
expect(result1.content).toContain('Missing required');
const result2 = await skillsMCP.executeTool('read_resource', {
file_path: 'test.md'
});
expect(result2.success).toBe(false);
expect(result2.content).toContain('Missing required');
});
});
describe('skills:list parameter handling', () => {
it('should work with no parameters', async () => {
// skills:list should work with empty params
const result = await skillsMCP.executeTool('list', {});
expect(result.success).toBe(true);
expect(result.content).toBeDefined();
});
it('should work with undefined params', async () => {
const result = await skillsMCP.executeTool('list', undefined);
// Should handle undefined gracefully
expect(result.content).toBeDefined();
});
});
describe('Positional argument mapping', () => {
it('explains how MCP maps positional to named arguments', () => {
// When Claude Code calls: await skills.find("pdf", 2)
// The MCP SDK uses the inputSchema parameter order:
// 1st positional "pdf" → query parameter
// 2nd positional 2 → depth parameter
// 3rd positional (if provided) → page parameter
// 4th positional (if provided) → limit parameter
// The executeTool receives: { query: "pdf", depth: 2 }
// So positional arguments ARE supported through MCP protocol!
const schema = {
type: 'object',
properties: {
query: { type: 'string' },
depth: { type: 'number' },
page: { type: 'number' },
limit: { type: 'number' }
}
};
// MCP protocol automatically maps positional args to schema parameter order
expect(schema.properties).toHaveProperty('query');
expect(schema.properties).toHaveProperty('depth');
expect(schema.properties).toHaveProperty('page');
expect(schema.properties).toHaveProperty('limit');
});
});
});