Skip to content

Commit 6eb40d5

Browse files
Trackerchumajimae
andauthored
Resource response data transformation to tabular-like LLM formatting, with command-line flag to revert back to JSON.
Co-authored-by: Ajima Chukwuemeka <32770340+ajimae@users.noreply.github.com>
1 parent e854cc4 commit 6eb40d5

24 files changed

Lines changed: 1953 additions & 53 deletions

File tree

.changeset/mean-carrots-grab.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@commercetools/agent-essentials": minor
3+
"@commercetools/mcp-essentials": minor
4+
---
5+
6+
Resource response data transformation to tabular-like LLM formatting, with command-line flag to revert back to JSON.

modelcontextprotocol/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Options = {
1919
businessUnitKey?: string;
2020
dynamicToolLoadingThreshold?: number;
2121
logging?: boolean;
22+
toolOutputFormat?: 'json' | 'tabular';
2223
};
2324

2425
type EnvVars = {
@@ -47,6 +48,7 @@ const PUBLIC_ARGS = [
4748
'projectKey',
4849
'apiUrl',
4950
'dynamicToolLoadingThreshold',
51+
'toolOutputFormat',
5052
];
5153

5254
const ACCEPTED_ARGS = [...PUBLIC_ARGS, ...HIDDEN_ARGS];
@@ -176,6 +178,7 @@ export function parseArgs(args: string[]): {options: Options; env: EnvVars} {
176178
const options: Options = {};
177179
const env: EnvVars = {};
178180

181+
// eslint-disable-next-line complexity
179182
args.forEach((arg) => {
180183
if (arg.startsWith('--')) {
181184
const [key, value] = arg.slice(2).split('=');
@@ -208,6 +211,10 @@ export function parseArgs(args: string[]): {options: Options; env: EnvVars} {
208211
options.isAdmin = value === 'true';
209212
} else if (key == 'dynamicToolLoadingThreshold') {
210213
options.dynamicToolLoadingThreshold = Number(value);
214+
} else if (key == 'toolOutputFormat') {
215+
if (value === 'json' || value === 'tabular') {
216+
options.toolOutputFormat = value;
217+
}
211218
} else if (key == 'logging') {
212219
options.logging = value == 'true';
213220
} else if (key == 'cartId') {
@@ -276,6 +283,15 @@ export function parseArgs(args: string[]): {options: Options; env: EnvVars} {
276283
(process.env.DYNAMIC_TOOL_LOADING_THRESHOLD
277284
? Number(process.env.DYNAMIC_TOOL_LOADING_THRESHOLD)
278285
: undefined);
286+
287+
if (
288+
(process.env.TOOL_OUTPUT_FORMAT &&
289+
process.env.TOOL_OUTPUT_FORMAT === 'tabular') ||
290+
process.env.TOOL_OUTPUT_FORMAT === 'json'
291+
) {
292+
options.toolOutputFormat = process.env.TOOL_OUTPUT_FORMAT;
293+
}
294+
279295
options.cartId = options.cartId || process.env.CART_ID;
280296

281297
// Validate required commercetools credentials based on auth type
@@ -357,6 +373,7 @@ export async function main() {
357373
customerId: options.customerId,
358374
isAdmin: options.isAdmin,
359375
dynamicToolLoadingThreshold: options.dynamicToolLoadingThreshold,
376+
toolOutputFormat: options.toolOutputFormat,
360377
cartId: options.cartId,
361378
storeKey: options.storeKey,
362379
businessUnitKey: options.businessUnitKey,

typescript/jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const config: Config = {
44
preset: 'ts-jest',
55
testEnvironment: 'node',
66
roots: ['<rootDir>/src'],
7-
testMatch: ['**/__tests__/**/*.ts?(x)', '**/test/**/*.ts?(x)'],
7+
testMatch: ['**/__tests__/**/*.test.ts?(x)', '**/test/**/*.test.ts?(x)'],
88
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
99
collectCoverage: true,
1010
coverageDirectory: 'coverage',

typescript/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"prettier": "prettier './**/*.{js,ts,md,html,css}' --write",
1212
"prettier-check": "prettier './**/*.{js,ts,md,html,css}' --check",
1313
"validate": "pnpm run lint --fix && pnpm run prettier && pnpm run test",
14-
"test": "jest"
14+
"test": "jest",
15+
"test:watch": "jest --watch --collectCoverage=false"
1516
},
1617
"exports": {
1718
"./ai-sdk": {

typescript/src/ai-sdk/__tests__/essentials.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {isToolAllowed} from '../../shared/configuration';
55
import {contextToTools} from '../../shared/tools'; // Assuming this is the source of all tools
66
import {z} from 'zod';
77
import {Configuration, Context} from '../../types/configuration';
8+
89
// Mock dependencies
910
jest.mock('../../shared/api');
1011
jest.mock('../tool');
@@ -43,8 +44,9 @@ jest.mock('../../shared/tools', () => {
4344
const tools = contextToTools({});
4445

4546
describe('CommercetoolsAgentEssentials with Admin tools', () => {
47+
const toolFormat = 'json';
4648
const mockConfiguration = {
47-
context: {isAdmin: true},
49+
context: {isAdmin: true, toolOutputFormat: toolFormat},
4850
actions: {cart: {read: true}, products: {read: true}},
4951
} as any;
5052
const mockCommercetoolsAPIInstance = {} as CommercetoolsAPI;
@@ -122,21 +124,24 @@ describe('CommercetoolsAgentEssentials with Admin tools', () => {
122124
mockCommercetoolsAPIInstance,
123125
tools[0].method,
124126
tools[0].description,
125-
expect.any(Object)
127+
expect.any(Object),
128+
toolFormat
126129
);
127130
// Detailed check for tool2 (namespace 'product', method 'tool2')
128131
expect(CommercetoolsTool).toHaveBeenCalledWith(
129132
mockCommercetoolsAPIInstance,
130133
tools[1].method,
131134
tools[1].description,
132-
expect.any(Object)
135+
expect.any(Object),
136+
toolFormat
133137
);
134138
// Ensure tool3 was filtered out
135139
expect(CommercetoolsTool).not.toHaveBeenCalledWith(
136140
expect.anything(),
137141
tools[2].method,
138142
expect.anything(),
139-
expect.anything()
143+
expect.anything(),
144+
toolFormat
140145
);
141146

142147
expect(Object.keys(agentEssentials.tools)).toContain('tool1');

typescript/src/ai-sdk/__tests__/tool.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('CommercetoolsTool', () => {
2727
param1: z.string().describe('Parameter 1'),
2828
param2: z.number().optional().describe('Parameter 2'),
2929
});
30+
const toolFormat = 'json';
3031

3132
beforeEach(() => {
3233
// Clear all instances and calls to constructor and all methods:
@@ -53,11 +54,13 @@ describe('CommercetoolsTool', () => {
5354
mockCommercetoolsAPI,
5455
testMethod,
5556
testDescription,
56-
testSchema
57+
testSchema,
58+
toolFormat
5759
) as any; // Cast to any to access execute for testing
5860

5961
const executeArgs = {param1: 'testValue'};
60-
mockCommercetoolsAPI.run.mockResolvedValue('API Result');
62+
const apiReturnValue = 'API Result';
63+
mockCommercetoolsAPI.run.mockResolvedValue(apiReturnValue);
6164

6265
const result = await coreToolConfig.execute(executeArgs);
6366

@@ -66,25 +69,31 @@ describe('CommercetoolsTool', () => {
6669
testMethod,
6770
executeArgs
6871
);
69-
expect(result).toBe('API Result');
72+
expect(result).toBe(
73+
`{"TEST METHOD RESULT":${JSON.stringify(apiReturnValue)}}`
74+
);
7075
});
7176

7277
it('should correctly handle execute with optional parameters', async () => {
7378
const coreToolConfig = CommercetoolsTool(
7479
mockCommercetoolsAPI,
7580
testMethod,
7681
testDescription,
77-
testSchema
82+
testSchema,
83+
toolFormat
7884
) as any;
7985

8086
const executeArgs = {param1: 'testValue', param2: 123};
87+
const apiReturnValue = 'API Result with optional';
8188
mockCommercetoolsAPI.run.mockResolvedValue('API Result with optional');
8289

8390
const result = await coreToolConfig.execute(executeArgs);
8491
expect(mockCommercetoolsAPI.run).toHaveBeenCalledWith(
8592
testMethod,
8693
executeArgs
8794
);
88-
expect(result).toBe('API Result with optional');
95+
expect(result).toBe(
96+
`{"TEST METHOD RESULT":${JSON.stringify(apiReturnValue)}}`
97+
);
8998
});
9099
});

typescript/src/ai-sdk/essentials.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class CommercetoolsAgentEssentials {
4040
this._commercetools,
4141
tool.method,
4242
tool.description,
43-
tool.parameters
43+
tool.parameters,
44+
configuration.context?.toolOutputFormat
4445
);
4546
});
4647
}

typescript/src/ai-sdk/tool.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ import type {Tool} from 'ai';
22
import {tool} from 'ai';
33
import {z} from 'zod';
44
import CommercetoolsAPI from '../shared/api';
5+
import {transformToolOutput} from '../modelcontextprotocol/transform';
56

67
export default function CommercetoolsTool(
78
commercetoolsAPI: CommercetoolsAPI,
89
method: string,
910
description: string,
10-
schema: z.ZodObject<any, any, any, any, {[x: string]: any}>
11+
schema: z.ZodObject<any, any, any, any, {[x: string]: any}>,
12+
toolOutputFormat?: 'json' | 'tabular'
1113
): Tool {
1214
return tool({
1315
description: description,
1416
parameters: schema,
15-
execute: (arg: z.output<typeof schema>) => {
16-
return commercetoolsAPI.run(method, arg);
17+
execute: async (arg: z.output<typeof schema>) => {
18+
const result = await commercetoolsAPI.run(method, arg);
19+
return transformToolOutput({
20+
title: `${method} result`,
21+
data: result,
22+
format: toolOutputFormat,
23+
});
1724
},
1825
});
1926
}

typescript/src/langchain/__tests__/essentials.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ jest.mock('../../shared/tools', () => {
5656
let mockToolDefinitions: any[]; // To hold the data for assertions
5757

5858
describe('CommercetoolsAgentEssentials (Langchain)', () => {
59+
const toolOutputFormat = 'json';
5960
const mockConfiguration = {
6061
enabledTools: ['cart', 'product.lcTool2'],
61-
context: {isAdmin: true},
62+
context: {isAdmin: true, toolOutputFormat},
6263
} as any;
6364
const mockCommercetoolsAPIInstance = {} as CommercetoolsAPI;
6465
const mockLangchainTool = new DynamicStructuredTool({
@@ -141,13 +142,15 @@ describe('CommercetoolsAgentEssentials (Langchain)', () => {
141142
mockCommercetoolsAPIInstance,
142143
mockToolDefinitions[0].method,
143144
mockToolDefinitions[0].description,
144-
expect.any(Object)
145+
expect.any(Object),
146+
toolOutputFormat
145147
);
146148
expect(CommercetoolsTool).toHaveBeenCalledWith(
147149
mockCommercetoolsAPIInstance,
148150
mockToolDefinitions[1].method,
149151
mockToolDefinitions[1].description,
150-
expect.any(Object)
152+
expect.any(Object),
153+
toolOutputFormat
151154
);
152155
expect(CommercetoolsTool).not.toHaveBeenCalledWith(
153156
expect.anything(),

typescript/src/langchain/__tests__/tool.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@ describe('CommercetoolsTool (Langchain)', () => {
6565
expect(toolArgs.schema).toBe(testSchema);
6666
});
6767

68-
it('should call commercetoolsAPI.run and stringify the result when func is called', async () => {
68+
it('should call commercetoolsAPI.run and transform the result when func is called', async () => {
6969
CommercetoolsTool(
7070
mockCommercetoolsAPI,
7171
testMethod,
7272
testDescription,
73-
testSchema
73+
testSchema,
74+
'json'
7475
);
7576
// Access the mock's call arguments through the imported DynamicStructuredTool
7677
const toolConstructorArgs = (DynamicStructuredTool as unknown as jest.Mock)
@@ -88,15 +89,18 @@ describe('CommercetoolsTool (Langchain)', () => {
8889
testMethod,
8990
executeArgs
9091
);
91-
expect(result).toBe(JSON.stringify(apiResultObject));
92+
expect(result).toBe(
93+
`{"LC TEST METHOD RESULT":${JSON.stringify(apiResultObject)}}`
94+
);
9295
});
9396

94-
it('should handle func with optional parameters and stringify result', async () => {
97+
it('should handle func with optional parameters and transform result', async () => {
9598
CommercetoolsTool(
9699
mockCommercetoolsAPI,
97100
testMethod,
98101
testDescription,
99-
testSchema
102+
testSchema,
103+
'json'
100104
);
101105
const toolConstructorArgs = (DynamicStructuredTool as unknown as jest.Mock)
102106
.mock.calls[0][0];
@@ -111,6 +115,8 @@ describe('CommercetoolsTool (Langchain)', () => {
111115
testMethod,
112116
executeArgs
113117
);
114-
expect(result).toBe(JSON.stringify(apiResultObject));
118+
expect(result).toBe(
119+
`{"LC TEST METHOD RESULT":${JSON.stringify(apiResultObject)}}`
120+
);
115121
});
116122
});

0 commit comments

Comments
 (0)