mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-02 07:12:41 +00:00
fix(mcp): validate aliases without schema combinators
This commit is contained in:
parent
f12493a97c
commit
eb684f51b7
4 changed files with 57 additions and 10 deletions
|
|
@ -163,10 +163,15 @@ function normalizeToolParams(
|
|||
const normalized = { ...input };
|
||||
for (const { canonical, aliases } of definitions) {
|
||||
const keys = [canonical, ...aliases];
|
||||
const supplied = keys.flatMap((key) => {
|
||||
const supplied: Array<{ key: string; value: string }> = [];
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
|
||||
const value = input[key];
|
||||
return typeof value === 'string' && value.trim() ? [{ key, value: value.trim() }] : [];
|
||||
});
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
return { error: `MCP parameter ${method}.${key} must be a non-empty string.` };
|
||||
}
|
||||
supplied.push({ key, value: value.trim() });
|
||||
}
|
||||
const distinctValues = new Set(supplied.map(({ value }) => value));
|
||||
if (distinctValues.size > 1) {
|
||||
return {
|
||||
|
|
@ -179,6 +184,14 @@ function normalizeToolParams(
|
|||
for (const alias of aliases) delete normalized[alias];
|
||||
if (supplied.length > 0) normalized[canonical] = supplied[0].value;
|
||||
}
|
||||
|
||||
if (
|
||||
method === 'impact' &&
|
||||
typeof normalized.target !== 'string' &&
|
||||
(typeof normalized.target_uid !== 'string' || !normalized.target_uid.trim())
|
||||
) {
|
||||
return { error: 'MCP impact requires target, name, symbol, or target_uid.' };
|
||||
}
|
||||
return { params: normalized };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -591,7 +591,6 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|||
},
|
||||
},
|
||||
required: ['direction'],
|
||||
anyOf: [{ required: ['target'] }, { required: ['name'] }, { required: ['symbol'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -391,6 +391,43 @@ describe('LocalBackend.callTool', () => {
|
|||
expect(resolveSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['impact', { target: '', direction: 'upstream' }],
|
||||
['impact', { name: 42, direction: 'upstream' }],
|
||||
['context', { name: 'validate', file: ' ' }],
|
||||
])('rejects invalid %s aliases before repository resolution', async (method, params) => {
|
||||
const resolveSpy = vi.spyOn(backend, 'resolveRepo');
|
||||
|
||||
const result = await backend.callTool(method, params);
|
||||
|
||||
expect(result.error).toMatch(/non-empty string/i);
|
||||
expect(resolveSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a missing impact target before repository resolution', async () => {
|
||||
const resolveSpy = vi.spyOn(backend, 'resolveRepo');
|
||||
|
||||
const result = await backend.callTool('impact', { direction: 'upstream' });
|
||||
|
||||
expect(result.error).toMatch(/requires target, name, symbol, or target_uid/i);
|
||||
expect(resolveSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves target_uid-only impact dispatch', async () => {
|
||||
const impactSpy = vi
|
||||
.spyOn(backend as any, 'impact')
|
||||
.mockResolvedValue({ status: 'normalized' });
|
||||
|
||||
await backend.callTool('impact', {
|
||||
target_uid: 'Function:src/auth.ts:validate',
|
||||
direction: 'upstream',
|
||||
});
|
||||
|
||||
expect(impactSpy.mock.calls[0][1]).toMatchObject({
|
||||
target_uid: 'Function:src/auth.ts:validate',
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes impact aliases before @group forwarding', async () => {
|
||||
resolveAtMemberMock.mockResolvedValue({ ok: true, repoPath: '/tmp/test-project' });
|
||||
const groupImpactSpy = vi
|
||||
|
|
|
|||
|
|
@ -144,17 +144,15 @@ describe('GITNEXUS_TOOLS', () => {
|
|||
expect(apiImpactTool.inputSchema.required).toEqual([]);
|
||||
});
|
||||
|
||||
it('impact tool requires direction and accepts target, name, or symbol', () => {
|
||||
it('impact tool requires direction and advertises target, name, or symbol without combinators', () => {
|
||||
const impactTool = GITNEXUS_TOOLS.find((t) => t.name === 'impact')!;
|
||||
expect(impactTool.inputSchema.required).toContain('direction');
|
||||
expect(impactTool.inputSchema.required).not.toContain('target');
|
||||
expect(impactTool.inputSchema.properties.name).toMatchObject({ type: 'string' });
|
||||
expect(impactTool.inputSchema.properties.symbol).toMatchObject({ type: 'string' });
|
||||
expect(impactTool.inputSchema.anyOf).toEqual([
|
||||
{ required: ['target'] },
|
||||
{ required: ['name'] },
|
||||
{ required: ['symbol'] },
|
||||
]);
|
||||
expect(impactTool.inputSchema).not.toHaveProperty('anyOf');
|
||||
expect(impactTool.inputSchema).not.toHaveProperty('oneOf');
|
||||
expect(impactTool.inputSchema).not.toHaveProperty('allOf');
|
||||
});
|
||||
|
||||
it('impact tool advertises the PDG-only `line` statement anchor (integer, min 0, not required)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue