@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
215 lines • 6.49 kB
JavaScript
/**
* Hashing utilities for tool identity and change detection
*/
import { createHash } from "crypto";
/**
* Hash algorithm to use
*/
const HASH_ALGORITHM = "sha256";
/**
* Tool hash utility class
*/
export class ToolHashUtils {
/**
* Calculate tool hash for identity and change detection
*/
static calculateToolHash(tool, serverName) {
const toolDef = "tool" in tool ? tool.tool : tool;
const hashData = {
name: toolDef.name,
serverName: serverName || tool.serverName,
inputSchema: toolDef.inputSchema,
outputSchema: toolDef.outputSchema,
annotations: toolDef.annotations,
};
return this.hashObject(hashData);
}
/**
* Calculate hash for a list of tools (server tools hash)
*/
static calculateServerToolsHash(tools) {
// Sort tools by name for consistent hashing
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
const toolHashes = sortedTools.map((tool) => this.calculateToolHash(tool));
return this.hashObject(toolHashes);
}
/**
* Compare tools and detect changes
*/
static detectToolChanges(previousTools, currentTools) {
const changes = [];
// Create lookup maps
const previousMap = new Map();
const currentMap = new Map();
// Populate maps using namespaced names as keys
for (const tool of previousTools) {
previousMap.set(tool.namespacedName, tool);
}
for (const tool of currentTools) {
currentMap.set(tool.namespacedName, tool);
}
// Check for additions and updates
for (const [namespacedName, currentTool] of currentMap) {
const previousTool = previousMap.get(namespacedName);
if (!previousTool) {
// New tool
changes.push({
tool: currentTool,
changeType: "added",
currentHash: currentTool.toolHash,
});
}
else {
// Check if tool updated
if (previousTool.toolHash !== currentTool.toolHash) {
changes.push({
tool: currentTool,
changeType: "updated",
previousHash: previousTool.toolHash,
currentHash: currentTool.toolHash,
});
}
else {
changes.push({
tool: currentTool,
changeType: "unchanged",
currentHash: currentTool.toolHash,
});
}
}
}
// Check for removals
for (const [namespacedName, previousTool] of previousMap) {
if (!currentMap.has(namespacedName)) {
changes.push({
tool: previousTool,
changeType: "removed",
previousHash: previousTool.toolHash,
});
}
}
return changes;
}
/**
* Create tool with hash from MCP tool definition
*/
static createHashedTool(toolDef, serverName, namespacedName) {
const now = new Date();
const toolHash = this.calculateToolHash(toolDef, serverName);
return {
name: toolDef.name,
serverName,
namespacedName,
tool: toolDef,
discoveredAt: now,
lastUpdated: now,
serverStatus: "connected",
toolHash,
};
}
/**
* Update tool hash after modification
*/
static updateToolHash(tool) {
return {
...tool,
lastUpdated: new Date(),
toolHash: this.calculateToolHash(tool),
};
}
/**
* Get summary of tool changes
*/
static summarizeChanges(changes) {
const summary = {
added: 0,
updated: 0,
removed: 0,
unchanged: 0,
total: changes.length,
};
for (const change of changes) {
summary[change.changeType]++;
}
return summary;
}
/**
* Hash an object to a hex string
*/
static hashObject(obj) {
const hash = createHash(HASH_ALGORITHM);
const serialized = JSON.stringify(obj);
hash.update(serialized);
return hash.digest("hex");
}
}
/**
* Tool hash manager for tracking tool changes over time
*/
export class ToolHashManager {
hashHistory = new Map(); // namespacedName -> hash history
maxHistorySize = 10;
/**
* Add tool hash to history
*/
addToHistory(tool) {
const key = tool.namespacedName;
if (!this.hashHistory.has(key)) {
this.hashHistory.set(key, []);
}
const history = this.hashHistory.get(key);
history.push(tool.toolHash);
// Limit history size
if (history.length > this.maxHistorySize) {
history.shift();
}
}
/**
* Get hash history for a tool
*/
getHistory(namespacedName) {
return this.hashHistory.get(namespacedName) || [];
}
/**
* Check if tool has changed since last known version
*/
hasChanged(tool) {
const history = this.getHistory(tool.namespacedName);
if (history.length === 0) {
return true; // New tool
}
const lastHash = history[history.length - 1];
return lastHash !== tool.toolHash;
}
/**
* Get tools that have changed
*/
getChangedTools(tools) {
return tools.filter((tool) => this.hasChanged(tool));
}
/**
* Clear history for a tool
*/
clearHistory(namespacedName) {
this.hashHistory.delete(namespacedName);
}
/**
* Clear all history
*/
clearAllHistory() {
this.hashHistory.clear();
}
/**
* Get statistics about hash history
*/
getStats() {
return {
totalTools: this.hashHistory.size,
averageHistorySize: this.hashHistory.size > 0
? Array.from(this.hashHistory.values()).reduce((sum, hist) => sum + hist.length, 0) / this.hashHistory.size
: 0,
maxHistorySize: this.maxHistorySize,
};
}
}
//# sourceMappingURL=hashUtils.js.map