Skip to content

Commit eddadb4

Browse files
committed
feat: add raw response logging with truncation for large API responses
- Add TransportResponse wrapper with rawResponsePath propagation - Implement response.util.ts for raw response file storage - Add truncateForAI function for large response handling (~40k chars limit) - Update controller and tool layers to pass rawResponsePath through stack - Improve test setup with jest.setup.ts - Update deprecated /rest/api/3/search to /rest/api/3/search/jql endpoint
1 parent 32fc4e5 commit eddadb4

11 files changed

Lines changed: 754 additions & 105 deletions

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"build": "tsc",
1717
"prepare": "npm run build && node scripts/ensure-executable.js",
1818
"postinstall": "node scripts/ensure-executable.js",
19+
"clean": "rm -rf dist coverage",
1920
"test": "jest",
2021
"test:coverage": "jest --coverage",
2122
"test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
@@ -33,7 +34,8 @@
3334
"dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
3435
"dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
3536
"start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
36-
"start:cli": "npm run build && node dist/index.js"
37+
"start:cli": "npm run build && node dist/index.js",
38+
"cli": "npm run build && node dist/index.js"
3739
},
3840
"keywords": [
3941
"mcp",
@@ -58,7 +60,7 @@
5860
"cli",
5961
"mcp-inspector"
6062
],
61-
"author": "",
63+
"author": "Andi Ashari",
6264
"license": "ISC",
6365
"devDependencies": {
6466
"@eslint/js": "^9.39.1",
@@ -111,6 +113,9 @@
111113
"jest": {
112114
"preset": "ts-jest",
113115
"testEnvironment": "node",
116+
"setupFilesAfterEnv": [
117+
"<rootDir>/src/utils/jest.setup.ts"
118+
],
114119
"testMatch": [
115120
"**/src/**/*.test.ts"
116121
],

src/controllers/atlassian.api.controller.ts

Lines changed: 27 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
fetchAtlassian,
3-
getAtlassianCredentials,
4-
} from '../utils/transport.util.js';
1+
import atlassianApiService from '../services/vendor.atlassian.api.service.js';
52
import { Logger } from '../utils/logger.util.js';
63
import { handleControllerError } from '../utils/error-handler.util.js';
74
import { ControllerResponse } from '../types/common.types.js';
@@ -10,15 +7,21 @@ import {
107
RequestWithBodyArgsType,
118
} from '../tools/atlassian.api.types.js';
129
import { applyJqFilter, toOutputString } from '../utils/jq.util.js';
13-
import { createAuthMissingError } from '../utils/error.util.js';
14-
15-
// Logger instance for this module
16-
const logger = Logger.forContext('controllers/atlassian.api.controller.ts');
1710

1811
/**
19-
* Supported HTTP methods for API requests
12+
* @namespace AtlassianApiController
13+
* @description Controller for handling generic Jira API requests.
14+
* Orchestrates calls to the Atlassian API service and handles
15+
* response formatting (JQ filtering, TOON/JSON output).
16+
*
17+
* Architecture:
18+
* - Tool → Controller (this file) → Service → Transport
19+
* - Controller handles: JQ filtering, output formatting, error context
20+
* - Service handles: Credentials, path normalization, API calls
2021
*/
21-
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
22+
23+
// Logger instance for this module
24+
const logger = Logger.forContext('controllers/atlassian.api.controller.ts');
2225

2326
/**
2427
* Output format type
@@ -42,45 +45,15 @@ interface RequestWithBodyOptions extends BaseRequestOptions {
4245
body?: Record<string, unknown>;
4346
}
4447

45-
/**
46-
* Normalizes the API path by ensuring it starts with /
47-
* @param path - The raw path provided by the user
48-
* @returns Normalized path
49-
*/
50-
function normalizePath(path: string): string {
51-
let normalizedPath = path;
52-
if (!normalizedPath.startsWith('/')) {
53-
normalizedPath = '/' + normalizedPath;
54-
}
55-
return normalizedPath;
56-
}
57-
58-
/**
59-
* Appends query parameters to a path
60-
* @param path - The base path
61-
* @param queryParams - Optional query parameters
62-
* @returns Path with query string appended
63-
*/
64-
function appendQueryParams(
65-
path: string,
66-
queryParams?: Record<string, string>,
67-
): string {
68-
if (!queryParams || Object.keys(queryParams).length === 0) {
69-
return path;
70-
}
71-
const queryString = new URLSearchParams(queryParams).toString();
72-
return path + (path.includes('?') ? '&' : '?') + queryString;
73-
}
74-
7548
/**
7649
* Shared handler for all HTTP methods
7750
*
7851
* @param method - HTTP method (GET, POST, PUT, PATCH, DELETE)
7952
* @param options - Request options including path, queryParams, body (for non-GET), and jq filter
80-
* @returns Promise with raw JSON response (optionally filtered)
53+
* @returns Promise with formatted response content
8154
*/
8255
async function handleRequest(
83-
method: HttpMethod,
56+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
8457
options: RequestWithBodyOptions,
8558
): Promise<ControllerResponse> {
8659
const methodLogger = logger.forMethod(`handle${method}`);
@@ -91,46 +64,28 @@ async function handleRequest(
9164
...(options.body && { bodyKeys: Object.keys(options.body) }),
9265
});
9366

94-
// Get credentials
95-
const credentials = getAtlassianCredentials();
96-
if (!credentials) {
97-
throw createAuthMissingError();
98-
}
99-
100-
// Normalize path and append query params
101-
let path = normalizePath(options.path);
102-
path = appendQueryParams(path, options.queryParams);
103-
104-
methodLogger.debug(`${method}ing: ${path}`);
105-
106-
const fetchOptions: {
107-
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
108-
body?: unknown;
109-
} = {
110-
method,
111-
};
112-
113-
// Add body for methods that support it
114-
if (options.body && ['POST', 'PUT', 'PATCH'].includes(method)) {
115-
fetchOptions.body = options.body;
116-
}
117-
118-
const response = await fetchAtlassian<unknown>(
119-
credentials,
120-
path,
121-
fetchOptions,
67+
// Call the service layer (returns TransportResponse with data and rawResponsePath)
68+
const response = await atlassianApiService.request<unknown>(
69+
options.path,
70+
{
71+
method,
72+
queryParams: options.queryParams,
73+
body: options.body,
74+
},
12275
);
123-
methodLogger.debug('Successfully received response');
76+
77+
methodLogger.debug('Successfully received response from service');
12478

12579
// Apply JQ filter if provided, otherwise return raw data
126-
const result = applyJqFilter(response, options.jq);
80+
const result = applyJqFilter(response.data, options.jq);
12781

12882
// Convert to output format (TOON by default, JSON if requested)
12983
const useToon = options.outputFormat !== 'json';
13084
const content = await toOutputString(result, useToon);
13185

13286
return {
13387
content,
88+
rawResponsePath: response.rawResponsePath,
13489
};
13590
} catch (error) {
13691
throw handleControllerError(error, {

0 commit comments

Comments
 (0)