Skip to content

Commit b999262

Browse files
committed
feat: enhance API response handling with custom path support and improved error messages
1 parent dc1c589 commit b999262

File tree

5 files changed

+145
-76
lines changed

5 files changed

+145
-76
lines changed

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,36 @@ extensions that provide fine-grained control over API interactions:
5454
- **`x-examples`**: Add request/response examples for better documentation
5555
- **`x-remap-path-to-header`**: Map path parameters to request headers
5656
- **`x-custom-base-url`**: Override base URL per operation
57+
- **`x-custom-path`**: Override operation path
5758
- **`x-sensitive-params`**: Mark sensitive data for automatic redaction
5859
- **`x-sensitive-response-fields`**: Mark response fields as sensitive
5960

6061
#### Response Processing Extensions
6162

6263
- **`x-response-config`**: Control response handling:
6364
- Maximum response length limits
64-
- `includeResponseKeys`: Specify which keys to include in the response (all others will be excluded)
65+
- `includeResponseKeys`: Specify which keys to include in the response (all
66+
others will be excluded)
6567
- Supports dot notation for nested fields (e.g., `user.profile.email`)
66-
- Supports wildcards: `*` for single level, `**` for all nested levels (e.g., `data.*.id`, `user.**`)
67-
- Single words without dots will match all properties with that name at any level
68+
- Supports wildcards: `*` for single level, `**` for all nested levels
69+
(e.g., `data.*.id`, `user.**`)
70+
- Single words without dots will match all properties with that name at any
71+
level
6872
- `excludeResponseKeys`: Specify which keys to exclude from the response
6973
- Supports dot notation for nested fields (e.g., `user.profile.address`)
70-
- Supports wildcards: `*` for single level, `**` for all nested levels (e.g., `data.*.secret`, `credentials.**`)
71-
- Single words without dots will match all properties with that name at any level (e.g., `secret` will exclude all properties named "secret" at any depth)
72-
- `sensitiveResponseFields`: Mark specific fields as sensitive (will be replaced with "\*SENSITIVE\*")
74+
- Supports wildcards: `*` for single level, `**` for all nested levels
75+
(e.g., `data.*.secret`, `credentials.**`)
76+
- Single words without dots will match all properties with that name at any
77+
level (e.g., `secret` will exclude all properties named "secret" at any
78+
depth)
79+
- `sensitiveResponseFields`: Mark specific fields as sensitive (will be
80+
replaced with "\*SENSITIVE\*")
7381
- Supports dot notation for nested fields (e.g., `user.token`)
74-
- Supports wildcards: `*` for single level, `**` for all nested levels (e.g., `*.password`, `**.secret`)
75-
- Single words without dots will match all properties with that name at any level (e.g., `password` will mask all properties named "password" at any depth)
82+
- Supports wildcards: `*` for single level, `**` for all nested levels
83+
(e.g., `*.password`, `**.secret`)
84+
- Single words without dots will match all properties with that name at any
85+
level (e.g., `password` will mask all properties named "password" at any
86+
depth)
7687
- **`x-tree-shaking-func`**: Custom response data filtering
7788

7889
### 📜 Script-Based Dynamic Values

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpc/oapi-invoker-mcp",
3-
"version": "0.1.4",
3+
"version": "0.1.5",
44
"description": "Invokes any OpenAPI through Model Context Protocol (MCP) server, supporting specification patches, custom authentication protocols, and data encryption/decryption",
55
"license": "MIT",
66
"repository": {

src/tool/invoker.ts

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export async function invoke(
9898
const path = p(extendTool.path!)(processedPathParams);
9999
const _op = (extendTool._rawOperation || {}) as Record<string, unknown>;
100100
const specificUrl = _op["x-custom-base-url"] as string | undefined;
101+
const customPathTemplate = _op["x-custom-path"] as string | undefined;
101102
const sensitiveParams =
102103
(_op["x-sensitive-params"] as Record<string, unknown>) ?? {};
103104

@@ -137,7 +138,7 @@ export async function invoke(
137138
}
138139

139140
if ((!specificUrl && !baseUrl) || !method || !path) {
140-
throw new Error("Invalid tool configuration");
141+
throw new Error("Invalid tool configuration, no URL/method/path");
141142
}
142143

143144
let requestHeaders = { ...headers };
@@ -162,9 +163,23 @@ export async function invoke(
162163
}
163164
}
164165

166+
// Determine final path: use x-custom-path (processed) if provided, otherwise use operation path
167+
const finalPath = customPathTemplate
168+
? p(customPathTemplate)(processedPathParams)
169+
: path;
170+
171+
// Update debug info tool.path to reflect final path when debug is enabled
172+
if (debugInfo) {
173+
debugInfo.tool.path = finalPath;
174+
}
175+
176+
if ((!specificUrl && !baseUrl) || !method || !finalPath) {
177+
throw new Error("Invalid tool configuration");
178+
}
179+
165180
let url = new URL(specificUrl ?? baseUrl);
166181

167-
const pathItems = path.split("/").slice(1);
182+
const pathItems = finalPath.split("/").slice(1);
168183
const pathRemaps = _op["x-remap-path-to-header"] as string[] | undefined;
169184
if (pathRemaps) {
170185
if (debugInfo) {
@@ -179,7 +194,7 @@ export async function invoke(
179194
} else {
180195
// Preserve base URL's path and append the tool path to support multi-level base URLs
181196
// Remove trailing slash from base path if present, then concatenate
182-
url.pathname = url.pathname.replace(/\/$/, "") + path;
197+
url.pathname = url.pathname.replace(/\/$/, "") + finalPath;
183198
}
184199

185200
// Separate query parameters from body parameters for all requests
@@ -246,7 +261,7 @@ export async function invoke(
246261
// @ts-ignore: generateTencentCloudSignature function signature may not match perfectly with Type`Script`
247262
requestHeaders = generateTencentCloudSignature(
248263
method,
249-
path,
264+
finalPath,
250265
url.searchParams,
251266
requestHeaders,
252267
requestBody,
@@ -362,7 +377,7 @@ export async function invoke(
362377
* Supports:
363378
* - "*" wildcard for matching all properties at the current level
364379
* - "**" wildcard for matching all properties at all nested levels
365-
*
380+
*
366381
* @param item The object to process
367382
* @param keys Array of path strings that may contain wildcards
368383
* @returns Array of expanded path strings with wildcards resolved to actual paths
@@ -392,11 +407,11 @@ function expandWildcardPaths(item: any, keys: string[]): string[] {
392407
* Recursively expands a path with wildcards
393408
*/
394409
function expandPathRecursive(
395-
obj: any,
396-
segments: string[],
397-
index: number,
398-
currentPath: string,
399-
result: string[]
410+
obj: any,
411+
segments: string[],
412+
index: number,
413+
currentPath: string,
414+
result: string[],
400415
): void {
401416
if (index >= segments.length) {
402417
if (currentPath.length > 0) {
@@ -439,7 +454,13 @@ function expandPathRecursive(
439454
for (const key in obj) {
440455
const newPath = `${currentPath}.${key}`;
441456
// Use get to safely access properties
442-
expandPathRecursive(get(obj, key), segments, index + 1, newPath, result);
457+
expandPathRecursive(
458+
get(obj, key),
459+
segments,
460+
index + 1,
461+
newPath,
462+
result,
463+
);
443464
}
444465
}
445466
return;
@@ -448,7 +469,13 @@ function expandPathRecursive(
448469
// Regular property
449470
if (isObject(obj) && !isNull(obj) && has(obj, segment)) {
450471
const newPath = `${currentPath}.${segment}`;
451-
expandPathRecursive(get(obj, segment), segments, index + 1, newPath, result);
472+
expandPathRecursive(
473+
get(obj, segment),
474+
segments,
475+
index + 1,
476+
newPath,
477+
result,
478+
);
452479
}
453480
}
454481

@@ -463,7 +490,7 @@ function findMatchingPropertyPaths(
463490
obj: any,
464491
key: string,
465492
currentPath: string = "",
466-
result: string[] = []
493+
result: string[] = [],
467494
): string[] {
468495
if (!isObject(obj) || isNull(obj)) {
469496
return result;
@@ -504,23 +531,23 @@ function transformItem(
504531
}
505532

506533
// Process keys that don't contain dots for recursive property name matching
507-
const processedExcludeKeys = excludeKeys.flatMap(key => {
534+
const processedExcludeKeys = excludeKeys.flatMap((key) => {
508535
// If key doesn't contain dots, find all paths with matching property name
509-
if (!key.includes('.') && !key.includes('*')) {
536+
if (!key.includes(".") && !key.includes("*")) {
510537
return findMatchingPropertyPaths(item, key);
511538
}
512539
return key;
513540
});
514541

515-
const processedIncludeKeys = includeKeys.flatMap(key => {
516-
if (!key.includes('.') && !key.includes('*')) {
542+
const processedIncludeKeys = includeKeys.flatMap((key) => {
543+
if (!key.includes(".") && !key.includes("*")) {
517544
return findMatchingPropertyPaths(item, key);
518545
}
519546
return key;
520547
});
521548

522-
const processedSensitiveKeys = sensitiveKeys.flatMap(key => {
523-
if (!key.includes('.') && !key.includes('*')) {
549+
const processedSensitiveKeys = sensitiveKeys.flatMap((key) => {
550+
if (!key.includes(".") && !key.includes("*")) {
524551
return findMatchingPropertyPaths(item, key);
525552
}
526553
return key;
@@ -529,7 +556,10 @@ function transformItem(
529556
// Expand wildcard paths in all key arrays
530557
const expandedIncludeKeys = expandWildcardPaths(item, processedIncludeKeys);
531558
const expandedExcludeKeys = expandWildcardPaths(item, processedExcludeKeys);
532-
const expandedSensitiveKeys = expandWildcardPaths(item, processedSensitiveKeys);
559+
const expandedSensitiveKeys = expandWildcardPaths(
560+
item,
561+
processedSensitiveKeys,
562+
);
533563

534564
/**
535565
* Step 1: Creates the initial processed item.
@@ -631,12 +661,10 @@ export function postProcess(
631661
return data;
632662
}
633663

634-
const includeResponseKeys: string[] =
635-
op["x-include-response-keys"] ||
664+
const includeResponseKeys: string[] = op["x-include-response-keys"] ||
636665
responseConfigGlobal["includeResponseKeys"] ||
637666
[];
638-
const excludeResponseKeys: string[] =
639-
op["x-exclude-response-keys"] ||
667+
const excludeResponseKeys: string[] = op["x-exclude-response-keys"] ||
640668
responseConfigGlobal["excludeResponseKeys"] ||
641669
[];
642670
const sensitiveResponseFields: string[] =
@@ -684,7 +712,8 @@ function truncateData(data: unknown, maxLength?: number): unknown {
684712
}
685713

686714
return {
687-
message: `Response was truncated (length: ${stringified.length}, max: ${maxLength})`,
715+
message:
716+
`Response was truncated (length: ${stringified.length}, max: ${maxLength})`,
688717
truncatedData: stringified.slice(0, maxLength) + "...",
689718
};
690719
}

src/tool/parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ const OperationExtensionSchema = z
153153
"x-remap-path-to-header": z
154154
.array(z.string().describe("Header key to remap to"))
155155
.optional(),
156+
"x-custom-path": z.string().optional(),
156157
"x-custom-base-url": z.string().optional(),
157158
"x-sensitive-params": z
158159
.record(z.string(), z.string())

0 commit comments

Comments
 (0)