touchdesigner-mcp-server
Version:
MCP server for TouchDesigner
97 lines (96 loc) • 4.1 kB
JavaScript
import { z } from "zod";
import { TOOL_NAMES } from "../../../core/constants.js";
import { handleToolError } from "../../../core/errorHandling.js";
import { buildToolMetadata, } from "../metadata/touchDesignerToolMetadata.js";
import { formatToolMetadata } from "../presenter/index.js";
import { TOOL_DEFINITIONS } from "../toolDefinitions.js";
import { detailOnlyFormattingSchema } from "../types.js";
const describeToolsSchema = detailOnlyFormattingSchema.extend({
filter: z
.string()
.min(1)
.describe("Optional keyword to filter by tool name, module path, or parameter description")
.optional(),
});
export function registerTdTools(server, logger, tdClient) {
// Register every TouchDesigner operation from the single source of truth.
for (const definition of TOOL_DEFINITIONS) {
server.registerTool(definition.name, {
description: definition.description,
inputSchema: definition.schema.strict(),
}, async (params = {}) => {
try {
const output = await definition.run({ logger, params, tdClient });
return createToolResult(tdClient, output);
}
catch (error) {
return handleToolError(error, logger, definition.name, definition.errorComment);
}
});
}
// `describe_td_tools` is the meta tool: it documents the tools above rather
// than calling TouchDesigner, so it is registered on its own.
const toolMetadataEntries = buildToolMetadata(TOOL_DEFINITIONS);
server.registerTool(TOOL_NAMES.DESCRIBE_TD_TOOLS, {
description: "Generate a filesystem-oriented manifest of available TouchDesigner tools",
inputSchema: describeToolsSchema.strict(),
}, async (params = {}) => {
try {
const { detailLevel, responseFormat, filter } = params;
const normalizedFilter = filter?.trim().toLowerCase();
const filteredEntries = normalizedFilter
? toolMetadataEntries.filter((entry) => matchesMetadataFilter(entry, normalizedFilter))
: toolMetadataEntries;
if (filteredEntries.length === 0) {
const message = filter
? `No TouchDesigner tools matched filter "${filter}".`
: "No TouchDesigner tools are registered.";
return {
content: [{ text: message, type: "text" }],
};
}
const formattedText = formatToolMetadata(filteredEntries, {
detailLevel: detailLevel ?? (filter ? "summary" : "minimal"),
filter: normalizedFilter,
responseFormat,
});
return {
content: [{ text: formattedText, type: "text" }],
};
}
catch (error) {
return handleToolError(error, logger, TOOL_NAMES.DESCRIBE_TD_TOOLS);
}
});
}
const createToolResult = (tdClient, output) => {
const content = typeof output === "string"
? [{ text: output, type: "text" }]
: output.content.map((block) => block.type === "image"
? {
data: block.data,
mimeType: block.mimeType,
type: "image",
}
: { text: block.text, type: "text" });
const additionalContents = tdClient.getAdditionalToolResultContents();
if (additionalContents) {
content.push(...additionalContents);
}
return { content };
};
function matchesMetadataFilter(entry, keyword) {
const normalizedKeyword = keyword.toLowerCase();
const haystacks = [
entry.functionName,
entry.modulePath,
entry.description,
entry.category,
entry.tool,
entry.notes ?? "",
];
if (haystacks.some((value) => value.toLowerCase().includes(normalizedKeyword))) {
return true;
}
return entry.parameters.some((param) => [param.name, param.type, param.description ?? ""].some((value) => value.toLowerCase().includes(normalizedKeyword)));
}