Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 151 additions & 1 deletion src/__tests__/integration/mcp-tool.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import {McpServerFactory} from '../../services/mcp-server-factory.service';
import {McpToolRegistry} from '../../services/mcp-tool-registry.service';
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {z} from 'zod';

Check failure on line 6 in src/__tests__/integration/mcp-tool.integration.ts

View workflow job for this annotation

GitHub Actions / node_matrix_tests (22)

'z' is defined but never used

Check failure on line 6 in src/__tests__/integration/mcp-tool.integration.ts

View workflow job for this annotation

GitHub Actions / node_matrix_tests (24)

'z' is defined but never used

Check failure on line 6 in src/__tests__/integration/mcp-tool.integration.ts

View workflow job for this annotation

GitHub Actions / node_matrix_tests (20)

'z' is defined but never used

Check warning on line 6 in src/__tests__/integration/mcp-tool.integration.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'z'.

See more on https://sonarcloud.io/project/issues?id=sourcefuse_loopback4-mcp&issues=AZ2vKzG-uBRwFKJ4eYCz&open=AZ2vKzG-uBRwFKJ4eYCz&pullRequest=15

// Test constants to avoid magic numbers
const TEST_NUMBER_1 = 1;
const TEST_NUMBER_2 = 2;
const TEST_NUMBER_3 = 3;
const TEST_NUMBERS = [TEST_NUMBER_1, TEST_NUMBER_2, TEST_NUMBER_3];

describe('McpServerFactory (integration)', () => {
let ctx: Context;
let toolRegistry: McpToolRegistry;
Expand All @@ -14,7 +20,7 @@
{
name: 'testTool',
description: 'Test tool',
schema: z.object({}),
schema: {},
handler: sinon.stub(),
},
];
Expand Down Expand Up @@ -51,4 +57,148 @@
sinon.match.func,
);
});

describe('parameter unwrapping', () => {
it('unwraps double-wrapped object parameters', async () => {
const receivedParams: Record<string, unknown> = {};

// Simulate the unwrapping logic directly
const parameters = {
currency: {
currency: {
currencyCode: 'CAD',
currencyName: 'Canadian Dollar',
symbol: 'C$',
country: 'Canada',
},
},
};

// Apply the same unwrapping logic from the factory
for (const [key, value] of Object.entries(parameters)) {
if (value && typeof value === 'object') {
const valueObj = value as Record<string, unknown>;
if (key in valueObj && Object.keys(valueObj).length === 1) {
receivedParams[key] = valueObj[key];
} else {
receivedParams[key] = value;
}
} else {
receivedParams[key] = value;
}
}

expect(receivedParams).to.deepEqual({
currency: {
currencyCode: 'CAD',
currencyName: 'Canadian Dollar',
symbol: 'C$',
country: 'Canada',
},
});
});

it('keeps normal object parameters unchanged', () => {
const receivedParams: Record<string, unknown> = {};

const normalParams = {
currency: {
currencyCode: 'USD',
currencyName: 'US Dollar',
symbol: '$',
country: 'USA',
},
amount: 100,
};

for (const [key, value] of Object.entries(normalParams)) {
if (value && typeof value === 'object') {
const valueObj = value as Record<string, unknown>;
if (key in valueObj && Object.keys(valueObj).length === 1) {
receivedParams[key] = valueObj[key];
} else {
receivedParams[key] = value;
}
} else {
receivedParams[key] = value;
}
}

expect(receivedParams).to.deepEqual(normalParams);
});

it('preserves primitive parameters unchanged', () => {
const receivedParams: Record<string, unknown> = {};

const primitiveParams = {
name: 'John Doe',
age: 30,
active: true,
};

for (const [key, value] of Object.entries(primitiveParams)) {
if (value && typeof value === 'object') {
const valueObj = value as Record<string, unknown>;
if (key in valueObj && Object.keys(valueObj).length === 1) {
receivedParams[key] = valueObj[key];
} else {
receivedParams[key] = value;
}
} else {
receivedParams[key] = value;
}
}

expect(receivedParams).to.deepEqual(primitiveParams);
});

it('handles array parameters correctly', () => {
const receivedParams: Record<string, unknown> = {};

const arrayParams: Record<string, unknown> = {
items: ['item1', 'item2', 'item3'],
numbers: TEST_NUMBERS,
};

for (const [key, value] of Object.entries(arrayParams)) {
if (value && typeof value === 'object') {
const valueObj = value as Record<string, unknown>;
if (key in valueObj && Object.keys(valueObj).length === 1) {
receivedParams[key] = valueObj[key];
} else {
receivedParams[key] = value;
}
} else {
receivedParams[key] = value;
}
}

expect(receivedParams).to.deepEqual(arrayParams);
});

it('handles null and undefined parameters', () => {
const receivedParams: Record<string, unknown> = {};

const nullParams = {
name: 'John Doe',
age: null,
address: undefined,
};

for (const [key, value] of Object.entries(nullParams)) {
if (value && typeof value === 'object') {
const valueObj = value as Record<string, unknown>;
if (key in valueObj && Object.keys(valueObj).length === 1) {
receivedParams[key] = valueObj[key];
} else {
receivedParams[key] = value;
}
} else {
receivedParams[key] = value;
}
}

expect(receivedParams).to.deepEqual(nullParams);
});
});
});
28 changes: 26 additions & 2 deletions src/services/mcp-server-factory.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,35 @@ export class McpServerFactory {
const toolDefinitions = this.toolRegistry.getToolDefinitions();
for (const toolDef of toolDefinitions) {
// Adapt the registry handler to work with the new API signature
// The new API expects (parameters, extra) instead of (context, args, extras)
const adaptedHandler = async (
parameters: Record<string, unknown>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => toolDef.handler(this.ctx, parameters, extra);
) => {
// Handle common double-wrapping patterns
const cleanedParameters: Record<string, unknown> = {};
for (const [key, value] of Object.entries(parameters)) {
// Skip non-objects and null/undefined values
if (!value || typeof value !== 'object') {
cleanedParameters[key] = value;
continue;
}

const valueObj = value as Record<string, unknown>;
// Pattern: Parameter value wrapped in object with same key
// e.g., "currency": {"currency": {...actual data...}}
cleanedParameters[key] =
key in valueObj && Object.keys(valueObj).length === 1
? valueObj[key]
: value;
}

const result = await toolDef.handler(
this.ctx,
cleanedParameters,
extra,
);
return result;
};

// Use the new registerTool API with type assertion to avoid deep type recursion
const registerTool = (
Expand Down
Loading