touchdesigner-mcp-server 1.1.1 ā 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +75 -26
- package/README.md +74 -25
- package/dist/core/constants.js +1 -0
- package/dist/features/tools/handlers/tdTools.js +162 -50
- package/dist/features/tools/metadata/touchDesignerToolMetadata.js +402 -0
- package/dist/features/tools/presenter/classListFormatter.js +187 -0
- package/dist/features/tools/presenter/index.js +11 -0
- package/dist/features/tools/presenter/markdownRenderer.js +25 -0
- package/dist/features/tools/presenter/nodeDetailsFormatter.js +140 -0
- package/dist/features/tools/presenter/nodeListFormatter.js +124 -0
- package/dist/features/tools/presenter/operationFormatter.js +117 -0
- package/dist/features/tools/presenter/presenter.js +62 -0
- package/dist/features/tools/presenter/responseFormatter.js +66 -0
- package/dist/features/tools/presenter/scriptResultFormatter.js +171 -0
- package/dist/features/tools/presenter/templates/markdown/classDetailsSummary.md +13 -0
- package/dist/features/tools/presenter/templates/markdown/classListSummary.md +7 -0
- package/dist/features/tools/presenter/templates/markdown/default.md +3 -0
- package/dist/features/tools/presenter/templates/markdown/detailedPayload.md +5 -0
- package/dist/features/tools/presenter/templates/markdown/nodeDetailsSummary.md +10 -0
- package/dist/features/tools/presenter/templates/markdown/nodeListSummary.md +8 -0
- package/dist/features/tools/presenter/templates/markdown/scriptSummary.md +15 -0
- package/dist/features/tools/presenter/toolMetadataFormatter.js +118 -0
- package/dist/features/tools/types.js +26 -1
- package/dist/gen/endpoints/TouchDesignerAPI.js +1 -1
- package/dist/gen/mcp/touchDesignerAPI.zod.js +1 -1
- package/dist/server/touchDesignerServer.js +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Details Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats TouchDesigner node parameter details with token optimization.
|
|
5
|
+
* Used by GET_TD_NODE_PARAMETERS tool.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js";
|
|
8
|
+
import { finalizeFormattedText, limitArray, mergeFormatterOptions, } from "./responseFormatter.js";
|
|
9
|
+
/**
|
|
10
|
+
* Format node parameter details
|
|
11
|
+
*/
|
|
12
|
+
export function formatNodeDetails(data, options) {
|
|
13
|
+
const opts = mergeFormatterOptions(options);
|
|
14
|
+
if (!data) {
|
|
15
|
+
return "No node details available.";
|
|
16
|
+
}
|
|
17
|
+
const nodePath = data.path;
|
|
18
|
+
const properties = data.properties;
|
|
19
|
+
const propertyKeys = properties ? Object.keys(properties) : [];
|
|
20
|
+
if (propertyKeys.length === 0) {
|
|
21
|
+
return `Node: ${nodePath}\nNo properties found.`;
|
|
22
|
+
}
|
|
23
|
+
if (opts.detailLevel === "detailed") {
|
|
24
|
+
return formatDetailed(data, opts.responseFormat);
|
|
25
|
+
}
|
|
26
|
+
let result;
|
|
27
|
+
if (opts.detailLevel === "minimal") {
|
|
28
|
+
result = formatMinimal(nodePath, propertyKeys, opts.limit);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
result = formatSummary(nodePath, data, opts.limit);
|
|
32
|
+
}
|
|
33
|
+
const context = result.context;
|
|
34
|
+
return finalizeFormattedText(result.text, opts, {
|
|
35
|
+
template: "nodeDetailsSummary",
|
|
36
|
+
context,
|
|
37
|
+
structured: context,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Minimal mode: Property names only
|
|
42
|
+
*/
|
|
43
|
+
function formatMinimal(nodePath, propertyKeys, limit) {
|
|
44
|
+
const { items, truncated } = limitArray(propertyKeys, limit);
|
|
45
|
+
let text = `Node: ${nodePath}\nProperties (${propertyKeys.length}):\n${items.join(", ")}`;
|
|
46
|
+
if (truncated) {
|
|
47
|
+
text += `\nš” ${propertyKeys.length - items.length} more properties omitted.`;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
text,
|
|
51
|
+
context: {
|
|
52
|
+
nodePath,
|
|
53
|
+
type: "",
|
|
54
|
+
id: 0,
|
|
55
|
+
name: "",
|
|
56
|
+
total: propertyKeys.length,
|
|
57
|
+
displayed: items.length,
|
|
58
|
+
properties: items.map((name) => ({ name, value: "" })),
|
|
59
|
+
truncated,
|
|
60
|
+
omittedCount: Math.max(propertyKeys.length - items.length, 0),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Summary mode: Key properties with values
|
|
66
|
+
*/
|
|
67
|
+
function formatSummary(nodePath, data, limit) {
|
|
68
|
+
const properties = data.properties || {};
|
|
69
|
+
const propertyEntries = Object.entries(properties);
|
|
70
|
+
const { items, truncated } = limitArray(propertyEntries, limit);
|
|
71
|
+
let text = `Node: ${nodePath}\n`;
|
|
72
|
+
text += `Type: ${data.opType} (ID: ${data.id})\n`;
|
|
73
|
+
text += `Name: ${data.name}\n`;
|
|
74
|
+
text += `\nProperties (${propertyEntries.length}):\n\n`;
|
|
75
|
+
const propsForContext = [];
|
|
76
|
+
for (const [key, value] of items) {
|
|
77
|
+
const formattedValue = formatPropertyValue(value);
|
|
78
|
+
text += ` ${key}: ${formattedValue}\n`;
|
|
79
|
+
propsForContext.push({ name: key, value: formattedValue });
|
|
80
|
+
}
|
|
81
|
+
if (truncated) {
|
|
82
|
+
text += `\nš” ${propertyEntries.length - items.length} more properties omitted.`;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
text,
|
|
86
|
+
context: {
|
|
87
|
+
nodePath,
|
|
88
|
+
type: data.opType,
|
|
89
|
+
id: data.id,
|
|
90
|
+
name: data.name,
|
|
91
|
+
total: propertyEntries.length,
|
|
92
|
+
displayed: items.length,
|
|
93
|
+
properties: propsForContext,
|
|
94
|
+
truncated,
|
|
95
|
+
omittedCount: Math.max(propertyEntries.length - items.length, 0),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detailed mode: Full JSON
|
|
101
|
+
*/
|
|
102
|
+
function formatDetailed(data, format) {
|
|
103
|
+
const title = `Node ${data.path ?? data.name ?? "details"}`;
|
|
104
|
+
const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
|
|
105
|
+
return presentStructuredData({
|
|
106
|
+
text: title,
|
|
107
|
+
detailLevel: "detailed",
|
|
108
|
+
structured: data,
|
|
109
|
+
context: {
|
|
110
|
+
title,
|
|
111
|
+
payloadFormat,
|
|
112
|
+
},
|
|
113
|
+
template: "detailedPayload",
|
|
114
|
+
}, payloadFormat);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Format property value for display
|
|
118
|
+
*/
|
|
119
|
+
function formatPropertyValue(value) {
|
|
120
|
+
if (value === undefined || value === null) {
|
|
121
|
+
return "(none)";
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
return value.length > 50 ? `"${value.substring(0, 50)}..."` : `"${value}"`;
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
if (value.length === 0)
|
|
131
|
+
return "[]";
|
|
132
|
+
if (value.length <= 3)
|
|
133
|
+
return `[${value.join(", ")}]`;
|
|
134
|
+
return `[${value.slice(0, 3).join(", ")}, ... +${value.length - 3}]`;
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === "object") {
|
|
137
|
+
return `{${Object.keys(value).length} keys}`;
|
|
138
|
+
}
|
|
139
|
+
return String(value);
|
|
140
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node List Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats TouchDesigner node lists with token-optimized output.
|
|
5
|
+
* Used by GET_TD_NODES tool.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js";
|
|
8
|
+
import { finalizeFormattedText, formatOmissionHint, limitArray, mergeFormatterOptions, } from "./responseFormatter.js";
|
|
9
|
+
/**
|
|
10
|
+
* Format node list based on detail level
|
|
11
|
+
*/
|
|
12
|
+
export function formatNodeList(data, options) {
|
|
13
|
+
const opts = mergeFormatterOptions(options);
|
|
14
|
+
if (!data?.nodes || data.nodes.length === 0) {
|
|
15
|
+
return "No nodes found.";
|
|
16
|
+
}
|
|
17
|
+
const nodes = data.nodes;
|
|
18
|
+
const totalCount = nodes.length;
|
|
19
|
+
const parentPath = data.parentPath ?? "project";
|
|
20
|
+
// Apply limit
|
|
21
|
+
const { items: limitedNodes, truncated } = limitArray(nodes, opts.limit);
|
|
22
|
+
if (opts.detailLevel === "detailed") {
|
|
23
|
+
return formatDetailed(nodes, data, opts.responseFormat, parentPath);
|
|
24
|
+
}
|
|
25
|
+
let result;
|
|
26
|
+
if (opts.detailLevel === "minimal") {
|
|
27
|
+
result = formatMinimal(limitedNodes, parentPath, totalCount, truncated);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
result = formatSummary(limitedNodes, parentPath, totalCount, truncated);
|
|
31
|
+
}
|
|
32
|
+
const hintEnabled = truncated && opts.includeHints;
|
|
33
|
+
let output = result.text;
|
|
34
|
+
if (hintEnabled) {
|
|
35
|
+
output += formatOmissionHint(totalCount, limitedNodes.length, "node");
|
|
36
|
+
}
|
|
37
|
+
const context = result.context;
|
|
38
|
+
context.truncated = hintEnabled;
|
|
39
|
+
if (!hintEnabled) {
|
|
40
|
+
context.omittedCount = 0;
|
|
41
|
+
}
|
|
42
|
+
return finalizeFormattedText(output, opts, {
|
|
43
|
+
template: "nodeListSummary",
|
|
44
|
+
context,
|
|
45
|
+
structured: context,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimal mode: Only node paths
|
|
50
|
+
*/
|
|
51
|
+
function formatMinimal(nodes, parentPath, totalCount, truncated) {
|
|
52
|
+
const paths = nodes.map((n) => n.path || n.name);
|
|
53
|
+
const text = `Found ${nodes.length} nodes in ${parentPath}:
|
|
54
|
+
${paths.join("\n")}`;
|
|
55
|
+
return {
|
|
56
|
+
text,
|
|
57
|
+
context: buildNodeListContext(nodes, parentPath, totalCount, truncated),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Summary mode: Essential info with types
|
|
62
|
+
*/
|
|
63
|
+
function formatSummary(nodes, parentPath, totalCount, truncated) {
|
|
64
|
+
const header = `Found ${totalCount} nodes in ${parentPath}:
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
const groups = buildGroups(nodes);
|
|
68
|
+
const sections = groups.map((group) => {
|
|
69
|
+
const nodeLines = group.nodes.map((n) => ` ⢠${n.name} [${n.path}]`);
|
|
70
|
+
return `${group.type}:
|
|
71
|
+
${nodeLines.join("\n")}`;
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
text: header + sections.join("\n\n"),
|
|
75
|
+
context: {
|
|
76
|
+
parentPath,
|
|
77
|
+
totalCount,
|
|
78
|
+
groups,
|
|
79
|
+
truncated,
|
|
80
|
+
omittedCount: Math.max(totalCount - nodes.length, 0),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Detailed mode: Full information (original behavior)
|
|
86
|
+
*/
|
|
87
|
+
function formatDetailed(nodes, data, format, parentPath) {
|
|
88
|
+
const title = `Nodes in ${parentPath}`;
|
|
89
|
+
const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
|
|
90
|
+
return presentStructuredData({
|
|
91
|
+
text: title,
|
|
92
|
+
detailLevel: "detailed",
|
|
93
|
+
structured: { ...data, nodes },
|
|
94
|
+
context: {
|
|
95
|
+
title,
|
|
96
|
+
payloadFormat,
|
|
97
|
+
},
|
|
98
|
+
template: "detailedPayload",
|
|
99
|
+
}, payloadFormat);
|
|
100
|
+
}
|
|
101
|
+
function buildNodeListContext(nodes, parentPath, totalCount, truncated) {
|
|
102
|
+
return {
|
|
103
|
+
parentPath,
|
|
104
|
+
totalCount,
|
|
105
|
+
groups: buildGroups(nodes),
|
|
106
|
+
truncated,
|
|
107
|
+
omittedCount: Math.max(totalCount - nodes.length, 0),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function buildGroups(nodes) {
|
|
111
|
+
const byType = new Map();
|
|
112
|
+
for (const node of nodes) {
|
|
113
|
+
const type = node.opType || "unknown";
|
|
114
|
+
if (!byType.has(type)) {
|
|
115
|
+
byType.set(type, []);
|
|
116
|
+
}
|
|
117
|
+
byType.get(type)?.push(node);
|
|
118
|
+
}
|
|
119
|
+
return Array.from(byType.entries()).map(([type, typeNodes]) => ({
|
|
120
|
+
type,
|
|
121
|
+
count: typeNodes.length,
|
|
122
|
+
nodes: typeNodes.map((n) => ({ name: n.name, path: n.path })),
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { finalizeFormattedText, mergeFormatterOptions, } from "./responseFormatter.js";
|
|
2
|
+
export function formatTdInfo(data, options) {
|
|
3
|
+
const opts = mergeFormatterOptions(options);
|
|
4
|
+
if (!data) {
|
|
5
|
+
return finalizeFormattedText("TouchDesigner info not available.", opts, {
|
|
6
|
+
context: { title: "TouchDesigner Info" },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
const summary = `Server: ${data.server}\nVersion: ${data.version}`;
|
|
10
|
+
const osLine = data.osName
|
|
11
|
+
? `\nOS: ${data.osName} ${data.osVersion ?? ""}`
|
|
12
|
+
: "";
|
|
13
|
+
const text = opts.detailLevel === "minimal"
|
|
14
|
+
? `Server: ${data.server}, v${data.version}`
|
|
15
|
+
: `${summary}${osLine}`;
|
|
16
|
+
return finalizeFormattedText(text.trim(), opts, {
|
|
17
|
+
template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
|
|
18
|
+
context: { title: "TouchDesigner Info", ...data },
|
|
19
|
+
structured: data,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function formatCreateNodeResult(data, options) {
|
|
23
|
+
const opts = mergeFormatterOptions(options);
|
|
24
|
+
const node = data?.result;
|
|
25
|
+
if (!node) {
|
|
26
|
+
return finalizeFormattedText("Node created but no metadata returned.", opts, {
|
|
27
|
+
context: { title: "Create Node" },
|
|
28
|
+
structured: data,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const name = node.name ?? "(unknown)";
|
|
32
|
+
const path = node.path ?? "(path unknown)";
|
|
33
|
+
const opType = node.opType ?? "unknown";
|
|
34
|
+
const base = `ā Created node '${name}' (${opType}) at ${path}`;
|
|
35
|
+
const propCount = Object.keys(node.properties ?? {}).length;
|
|
36
|
+
const text = opts.detailLevel === "minimal"
|
|
37
|
+
? base
|
|
38
|
+
: `${base}\nProperties detected: ${propCount}`;
|
|
39
|
+
return finalizeFormattedText(text, opts, {
|
|
40
|
+
template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
|
|
41
|
+
context: { title: "Create Node", path, opType },
|
|
42
|
+
structured: data,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function formatUpdateNodeResult(data, options) {
|
|
46
|
+
const opts = mergeFormatterOptions(options);
|
|
47
|
+
const updatedCount = data?.updated?.length ?? 0;
|
|
48
|
+
const failedCount = data?.failed?.length ?? 0;
|
|
49
|
+
const base = `ā Updated ${updatedCount} parameter(s)`;
|
|
50
|
+
const text = opts.detailLevel === "minimal"
|
|
51
|
+
? base
|
|
52
|
+
: `${base}${failedCount ? `, ${failedCount} failed` : ""}`;
|
|
53
|
+
const context = {
|
|
54
|
+
title: "Update Node",
|
|
55
|
+
updated: data?.updated,
|
|
56
|
+
failed: data?.failed,
|
|
57
|
+
message: data?.message,
|
|
58
|
+
};
|
|
59
|
+
return finalizeFormattedText(text, opts, {
|
|
60
|
+
template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
|
|
61
|
+
context,
|
|
62
|
+
structured: data,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function formatDeleteNodeResult(data, options) {
|
|
66
|
+
const opts = mergeFormatterOptions(options);
|
|
67
|
+
const deleted = data?.deleted ?? false;
|
|
68
|
+
const name = data?.node?.name ?? "node";
|
|
69
|
+
const path = data?.node?.path ?? "(path unknown)";
|
|
70
|
+
const text = deleted
|
|
71
|
+
? `šļø Deleted '${name}' at ${path}`
|
|
72
|
+
: `Deletion status unknown for '${name}' at ${path}`;
|
|
73
|
+
return finalizeFormattedText(text, opts, {
|
|
74
|
+
template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
|
|
75
|
+
context: { title: "Delete Node", path, deleted },
|
|
76
|
+
structured: data,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export function formatExecNodeMethodResult(data, context, options) {
|
|
80
|
+
const opts = mergeFormatterOptions(options);
|
|
81
|
+
const callSignature = buildCallSignature(context);
|
|
82
|
+
const resultPreview = summarizeValue(data?.result);
|
|
83
|
+
const text = `${callSignature}\nResult: ${resultPreview}`;
|
|
84
|
+
return finalizeFormattedText(text, opts, {
|
|
85
|
+
template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
|
|
86
|
+
context: { title: "Execute Node Method", callSignature },
|
|
87
|
+
structured: { ...context, result: data?.result },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function buildCallSignature(params) {
|
|
91
|
+
const argPart = params.args ?? [];
|
|
92
|
+
const kwPart = params.kwargs
|
|
93
|
+
? Object.entries(params.kwargs).map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
94
|
+
: [];
|
|
95
|
+
const joinedArgs = [...argPart.map(stringifyValue), ...kwPart].join(", ");
|
|
96
|
+
return `op('${params.nodePath}').${params.method}(${joinedArgs})`;
|
|
97
|
+
}
|
|
98
|
+
function summarizeValue(value) {
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return "(no result)";
|
|
101
|
+
if (value === null)
|
|
102
|
+
return "null";
|
|
103
|
+
if (typeof value === "string")
|
|
104
|
+
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
|
105
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
106
|
+
return String(value);
|
|
107
|
+
if (Array.isArray(value))
|
|
108
|
+
return `Array[${value.length}]`;
|
|
109
|
+
if (typeof value === "object")
|
|
110
|
+
return `Object{${Object.keys(value).length} keys}`;
|
|
111
|
+
return String(value);
|
|
112
|
+
}
|
|
113
|
+
function stringifyValue(value) {
|
|
114
|
+
if (typeof value === "string")
|
|
115
|
+
return `'${value}'`;
|
|
116
|
+
return JSON.stringify(value);
|
|
117
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { stringify as toYaml } from "yaml";
|
|
2
|
+
import { renderMarkdownTemplate } from "./markdownRenderer.js";
|
|
3
|
+
export const DEFAULT_PRESENTER_FORMAT = "yaml";
|
|
4
|
+
export function presentStructuredData(payload, format = DEFAULT_PRESENTER_FORMAT) {
|
|
5
|
+
switch (format) {
|
|
6
|
+
case "json":
|
|
7
|
+
return JSON.stringify(resolveStructured(payload), null, 2);
|
|
8
|
+
case "yaml":
|
|
9
|
+
return toYaml(resolveStructured(payload));
|
|
10
|
+
case "markdown":
|
|
11
|
+
return formatMarkdown(payload, format);
|
|
12
|
+
default:
|
|
13
|
+
return toYaml(resolveStructured(payload));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function resolveStructured(payload) {
|
|
17
|
+
if (payload.structured !== undefined) {
|
|
18
|
+
return payload.structured;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
mode: payload.detailLevel,
|
|
22
|
+
text: payload.text,
|
|
23
|
+
...(payload.context ?? {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function formatMarkdown(payload, requestedFormat) {
|
|
27
|
+
const context = { ...(payload.context ?? {}) };
|
|
28
|
+
const inferredTitle = payload.detailLevel
|
|
29
|
+
? `${payload.detailLevel.charAt(0).toUpperCase()}${payload.detailLevel.slice(1)}`
|
|
30
|
+
: "Response";
|
|
31
|
+
const effectiveTemplate = payload.template ??
|
|
32
|
+
(payload.detailLevel === "detailed" ? "detailedPayload" : "default");
|
|
33
|
+
const payloadFormat = context.payloadFormat ??
|
|
34
|
+
(requestedFormat === "markdown"
|
|
35
|
+
? DEFAULT_PRESENTER_FORMAT
|
|
36
|
+
: requestedFormat);
|
|
37
|
+
const resolveStructuredText = () => {
|
|
38
|
+
if (typeof payload.structured === "string")
|
|
39
|
+
return payload.structured;
|
|
40
|
+
if (payload.structured === undefined)
|
|
41
|
+
return payload.text ?? "";
|
|
42
|
+
return payloadFormat === "json"
|
|
43
|
+
? JSON.stringify(payload.structured, null, 2)
|
|
44
|
+
: toYaml(payload.structured);
|
|
45
|
+
};
|
|
46
|
+
let bodyText = payload.text?.trim() ?? "";
|
|
47
|
+
if (!bodyText) {
|
|
48
|
+
bodyText = resolveStructuredText();
|
|
49
|
+
}
|
|
50
|
+
if (effectiveTemplate === "detailedPayload") {
|
|
51
|
+
context.title ??= inferredTitle;
|
|
52
|
+
context.payload ??= resolveStructuredText();
|
|
53
|
+
context.payloadFormat ??= payloadFormat;
|
|
54
|
+
}
|
|
55
|
+
if (effectiveTemplate === "default" && !("title" in context)) {
|
|
56
|
+
context.title = inferredTitle;
|
|
57
|
+
}
|
|
58
|
+
return renderMarkdownTemplate(effectiveTemplate, {
|
|
59
|
+
...context,
|
|
60
|
+
text: bodyText,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Formatter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides token-optimized formatting for MCP tool responses.
|
|
5
|
+
* Based on the design document: docs/context-optimization-design.md
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Common formatting options
|
|
9
|
+
*/
|
|
10
|
+
import { presentStructuredData } from "./presenter.js";
|
|
11
|
+
/**
|
|
12
|
+
* Default formatter options
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_FORMATTER_OPTIONS = {
|
|
15
|
+
detailLevel: "summary",
|
|
16
|
+
limit: undefined,
|
|
17
|
+
includeHints: true,
|
|
18
|
+
responseFormat: undefined,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Merge user options with defaults
|
|
22
|
+
*/
|
|
23
|
+
export function mergeFormatterOptions(options) {
|
|
24
|
+
const merged = { ...DEFAULT_FORMATTER_OPTIONS, ...options };
|
|
25
|
+
const detailLevel = options?.detailLevel ??
|
|
26
|
+
options?.mode ??
|
|
27
|
+
DEFAULT_FORMATTER_OPTIONS.detailLevel;
|
|
28
|
+
return {
|
|
29
|
+
detailLevel,
|
|
30
|
+
limit: merged.limit,
|
|
31
|
+
includeHints: merged.includeHints ?? true,
|
|
32
|
+
responseFormat: merged.responseFormat,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function finalizeFormattedText(text, opts, metadata) {
|
|
36
|
+
const chosenFormat = opts.responseFormat ??
|
|
37
|
+
(opts.detailLevel === "detailed" ? "yaml" : "markdown");
|
|
38
|
+
return presentStructuredData({
|
|
39
|
+
text,
|
|
40
|
+
detailLevel: opts.detailLevel,
|
|
41
|
+
structured: metadata?.structured,
|
|
42
|
+
context: metadata?.context,
|
|
43
|
+
template: metadata?.template,
|
|
44
|
+
}, chosenFormat);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Format a hint message for omitted content
|
|
48
|
+
*/
|
|
49
|
+
export function formatOmissionHint(totalCount, shownCount, itemType) {
|
|
50
|
+
const omitted = totalCount - shownCount;
|
|
51
|
+
if (omitted <= 0)
|
|
52
|
+
return "";
|
|
53
|
+
return `\nš” ${omitted} more ${itemType}(s) omitted. Use detailLevel='detailed' or increase limit to see all.`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Truncate array based on limit option
|
|
57
|
+
*/
|
|
58
|
+
export function limitArray(items, limit) {
|
|
59
|
+
if (limit === undefined || limit >= items.length) {
|
|
60
|
+
return { items, truncated: false };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
items: items.slice(0, limit),
|
|
64
|
+
truncated: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Result Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats Python script execution results with token optimization.
|
|
5
|
+
* Used by EXECUTE_PYTHON_SCRIPT tool.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js";
|
|
8
|
+
import { finalizeFormattedText, mergeFormatterOptions, } from "./responseFormatter.js";
|
|
9
|
+
/**
|
|
10
|
+
* Format script execution result
|
|
11
|
+
*/
|
|
12
|
+
export function formatScriptResult(data, scriptSnippet, options) {
|
|
13
|
+
const opts = mergeFormatterOptions(options);
|
|
14
|
+
if (!data) {
|
|
15
|
+
return "No result returned.";
|
|
16
|
+
}
|
|
17
|
+
const success = data.success ?? true;
|
|
18
|
+
const result = data.data?.result;
|
|
19
|
+
const output = data.data?.output;
|
|
20
|
+
const error = data.data?.error;
|
|
21
|
+
// Error case - always show full details
|
|
22
|
+
if (!success || error) {
|
|
23
|
+
return formatError(error, scriptSnippet);
|
|
24
|
+
}
|
|
25
|
+
if (opts.detailLevel === "detailed") {
|
|
26
|
+
return formatDetailed(data, opts.responseFormat);
|
|
27
|
+
}
|
|
28
|
+
let formattedText = "";
|
|
29
|
+
let context;
|
|
30
|
+
switch (opts.detailLevel) {
|
|
31
|
+
case "minimal":
|
|
32
|
+
formattedText = formatMinimal(result);
|
|
33
|
+
context = buildScriptContext(scriptSnippet, result, output);
|
|
34
|
+
break;
|
|
35
|
+
case "summary": {
|
|
36
|
+
const summary = formatSummary(result, output, scriptSnippet);
|
|
37
|
+
formattedText = summary.text;
|
|
38
|
+
context = summary.context;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const ctx = context;
|
|
43
|
+
return finalizeFormattedText(formattedText, opts, {
|
|
44
|
+
template: "scriptSummary",
|
|
45
|
+
context: ctx,
|
|
46
|
+
structured: ctx,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format error result
|
|
51
|
+
*/
|
|
52
|
+
function formatError(error, scriptSnippet) {
|
|
53
|
+
const errorMsg = typeof error === "string" ? error : JSON.stringify(error);
|
|
54
|
+
const snippet = scriptSnippet
|
|
55
|
+
? `\nScript: ${truncateScript(scriptSnippet)}`
|
|
56
|
+
: "";
|
|
57
|
+
return `ā Script execution failed:${snippet}\n\nError: ${errorMsg}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Minimal mode: Just the result value
|
|
61
|
+
*/
|
|
62
|
+
function formatMinimal(result) {
|
|
63
|
+
if (result === undefined || result === null) {
|
|
64
|
+
return "ā Script executed successfully (no return value)";
|
|
65
|
+
}
|
|
66
|
+
if (typeof result === "string" ||
|
|
67
|
+
typeof result === "number" ||
|
|
68
|
+
typeof result === "boolean") {
|
|
69
|
+
return `ā Result: ${result}`;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(result)) {
|
|
72
|
+
return `ā Result: Array[${result.length}]`;
|
|
73
|
+
}
|
|
74
|
+
if (typeof result === "object") {
|
|
75
|
+
const keys = Object.keys(result);
|
|
76
|
+
return `ā Result: Object{${keys.length} keys}`;
|
|
77
|
+
}
|
|
78
|
+
return `ā Result: ${String(result)}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Summary mode: Result with context
|
|
82
|
+
*/
|
|
83
|
+
function formatSummary(result, output, scriptSnippet) {
|
|
84
|
+
let formatted = "ā Script executed successfully\n\n";
|
|
85
|
+
const snippet = scriptSnippet ? truncateScript(scriptSnippet) : "";
|
|
86
|
+
if (snippet) {
|
|
87
|
+
formatted += `Script: ${snippet}\n\n`;
|
|
88
|
+
}
|
|
89
|
+
let resultPreview = "(none)";
|
|
90
|
+
if (result !== undefined && result !== null) {
|
|
91
|
+
resultPreview = formatResultValue(result, 500);
|
|
92
|
+
formatted += `Result: ${resultPreview}\n`;
|
|
93
|
+
}
|
|
94
|
+
let outputPreview;
|
|
95
|
+
if (output?.trim()) {
|
|
96
|
+
outputPreview =
|
|
97
|
+
output.length > 200 ? `${output.substring(0, 200)}...` : output;
|
|
98
|
+
formatted += `\nOutput:\n${outputPreview}`;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
text: formatted,
|
|
102
|
+
context: {
|
|
103
|
+
snippet,
|
|
104
|
+
resultType: getValueType(result),
|
|
105
|
+
resultPreview,
|
|
106
|
+
hasOutput: Boolean(outputPreview),
|
|
107
|
+
outputType: getValueType(output),
|
|
108
|
+
outputPreview,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function buildScriptContext(scriptSnippet, result, output) {
|
|
113
|
+
return {
|
|
114
|
+
snippet: scriptSnippet ? truncateScript(scriptSnippet) : "",
|
|
115
|
+
resultType: getValueType(result),
|
|
116
|
+
resultPreview: formatResultValue(result ?? "", 200),
|
|
117
|
+
hasOutput: Boolean(output?.trim()),
|
|
118
|
+
outputType: getValueType(output),
|
|
119
|
+
outputPreview: output,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Detailed mode: Full JSON
|
|
124
|
+
*/
|
|
125
|
+
function formatDetailed(data, format) {
|
|
126
|
+
const title = "Script Result";
|
|
127
|
+
const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
|
|
128
|
+
return presentStructuredData({
|
|
129
|
+
text: title,
|
|
130
|
+
detailLevel: "detailed",
|
|
131
|
+
structured: data,
|
|
132
|
+
context: {
|
|
133
|
+
title,
|
|
134
|
+
payloadFormat,
|
|
135
|
+
},
|
|
136
|
+
template: "detailedPayload",
|
|
137
|
+
}, payloadFormat);
|
|
138
|
+
}
|
|
139
|
+
function getValueType(value) {
|
|
140
|
+
if (value === undefined || value === null) {
|
|
141
|
+
return "none";
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return `array(${value.length})`;
|
|
145
|
+
}
|
|
146
|
+
return typeof value;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format result value with size limit
|
|
150
|
+
*/
|
|
151
|
+
function formatResultValue(value, maxChars) {
|
|
152
|
+
const str = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
|
|
153
|
+
if (str.length <= maxChars) {
|
|
154
|
+
return str;
|
|
155
|
+
}
|
|
156
|
+
return `${str.substring(0, maxChars)}...\nš” Result truncated. Use detailLevel='detailed' for full output.`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Truncate script for display
|
|
160
|
+
*/
|
|
161
|
+
function truncateScript(script, maxLength = 100) {
|
|
162
|
+
const trimmed = script.trim();
|
|
163
|
+
if (trimmed.length <= maxLength) {
|
|
164
|
+
return trimmed;
|
|
165
|
+
}
|
|
166
|
+
const firstLine = trimmed.split("\n")[0];
|
|
167
|
+
if (firstLine.length <= maxLength) {
|
|
168
|
+
return `${firstLine}...`;
|
|
169
|
+
}
|
|
170
|
+
return `${trimmed.substring(0, maxLength)}...`;
|
|
171
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Class `{{name}}`
|
|
2
|
+
- Type: {{type}}
|
|
3
|
+
- Description: {{{description}}}
|
|
4
|
+
|
|
5
|
+
### Methods ({{methodsShown}} / {{methodsTotal}})
|
|
6
|
+
{{#methods}}1. `{{signature}}` ā {{summary}}
|
|
7
|
+
{{/methods}}
|
|
8
|
+
|
|
9
|
+
### Properties ({{propertiesShown}} / {{propertiesTotal}})
|
|
10
|
+
{{#properties}}- `{{name}}`: {{type}}
|
|
11
|
+
{{/properties}}
|
|
12
|
+
|
|
13
|
+
{{#truncated}}_š” Additional members omitted._{{/truncated}}
|