Skip to content

feat: add UTCP provider for direct tool calling#532

Open
buger wants to merge 8 commits intomainfrom
buger/utcp-support
Open

feat: add UTCP provider for direct tool calling#532
buger wants to merge 8 commits intomainfrom
buger/utcp-support

Conversation

@buger
Copy link
Contributor

@buger buger commented Mar 12, 2026

Summary

Implement full UTCP (Universal Tool Calling Protocol) support as a new check provider type. UTCP is a client-side protocol that lets Visor call external tools directly via their native protocols (HTTP, CLI, SSE) without intermediate servers.

Changes

Provider Implementation:

  • New utcp-check-provider.ts (~700 lines) with full client lifecycle management
  • Three-way manual resolution: URL discovery, file-based, inline call templates
  • Smart tool name resolution with suffix matching for user convenience
  • Liquid template rendering in method arguments; environment variable support
  • Output transforms via Liquid and JavaScript; automatic issue extraction

Configuration & Registry:

  • Added 'utcp' to ConfigCheckType union and validCheckTypes array
  • Registered UtcpCheckProvider in provider registry
  • Added @utcp/sdk and @utcp/http as optional dependencies

Documentation:

  • Created comprehensive docs/utcp-provider.md (299 lines) with reference guide, examples, and UTCP vs MCP comparison
  • Updated 6 existing docs to reference UTCP and reflect 17 total providers
  • Updated CLAUDE.md with UTCP provider references

Testing:

  • 31 unit tests for UtcpCheckProvider (config, execution, transforms, issue extraction)
  • Updated registry test (17 providers)
  • All 3731 tests passing

Verification:

  • Real integration tests with UTCP SDK against httpbin.org
  • 3 successful checks: get_ip, get_uuid, echo_post
  • Liquid template interpolation and file-based manual discovery confirmed

Test Plan

  • ✅ Build: npm run build (no dist/ commits)
  • ✅ Lint: npm run lint
  • ✅ Tests: npm test (3731 passing)
  • ✅ Real integration: CLI execution with UTCP checks

🤖 Generated with Claude Code

Test and others added 2 commits March 12, 2026 22:17
Implement full UTCP (Universal Tool Calling Protocol) support as a new check provider type.

Provider Implementation:
- New utcp-check-provider.ts with full UTCP client lifecycle, three-way manual resolution (URL/file/inline), smart tool name resolution, Liquid template rendering in arguments, and automatic issue extraction
- Configuration support: 'utcp' added to ConfigCheckType and validCheckTypes
- Registered in CheckProviderRegistry with optional dependencies @utcp/sdk and @utcp/http

Documentation:
- Created docs/utcp-provider.md with complete reference guide
- Updated README.md, docs/configuration.md, glossary.md, pluggable.md, architecture.md, migration.md, and CLAUDE.md to reference UTCP (17 providers total now)

Testing:
- 31 unit tests for UtcpCheckProvider covering validation, execution, transforms, and issue extraction
- Updated check-provider-registry.test.ts (17 providers)
- All 3731 tests passing

Real Integration:
- Verified with actual UTCP SDK calls to httpbin.org (3 successful checks: get_ip, get_uuid, echo_post)
- Liquid template rendering and file-based manual discovery with suffix matching confirmed

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add examples/utcp-provider-example.yaml with 10 examples covering:
- URL-based manual discovery (OpenAPI/UTCP endpoints)
- Inline call templates
- File-based UTCP manuals
- Liquid templates in arguments
- Variable-based authentication
- JavaScript transforms for issue extraction
- Chaining UTCP with AI analysis
- Dynamic args via argsTransform
- Conditional execution with if expressions
- Multi-step pipeline with dependencies

Also link the example from README.md and docs/utcp-provider.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@probelabs
Copy link
Contributor

probelabs bot commented Mar 12, 2026

PR Overview: UTCP Provider for Direct Tool Calling

Summary

This PR adds full support for the UTCP (Universal Tool Calling Protocol) as a new check provider type in Visor. UTCP is a client-side protocol that enables direct tool calling via native protocols (HTTP, CLI, SSE) without intermediate servers. Tools publish JSON "manuals" describing how to call them, and the UTCP client makes direct calls.

Files Changed Analysis

29 files changed with +2,683 / -169 lines:

New Files (6)

  • src/providers/utcp-check-provider.ts (+647 lines) - Core UTCP provider implementation
  • src/utils/issue-normalizer.ts (+162 lines) - Shared issue extraction utilities
  • docs/utcp-provider.md (+343 lines) - Comprehensive documentation
  • examples/utcp-provider-example.yaml (+290 lines) - 12 usage examples
  • tests/unit/providers/utcp-check-provider.test.ts (+503 lines) - Unit tests
  • tests/unit/providers/utcp-mcp-bridge.test.ts (+307 lines) - AI integration tests

Modified Files (23)

  • Registry & Config: Added utcp to ConfigCheckType, validCheckTypes, and provider registry
  • AI Provider: Extended ai_mcp_servers to support type: utcp entries for AI tool exposure
  • MCP SSE Server: Added UTCP tool execution routing alongside workflow and http_client tools
  • MCP Provider: Refactored to use shared issue-normalizer utilities (-144 lines)
  • Documentation: Updated 7 docs to reference UTCP and reflect 17 total providers
  • Dependencies: Added @utcp/sdk and @utcp/http as optional dependencies

Architecture & Impact Assessment

What This PR Accomplishes

  1. New Provider Type: Adds type: utcp check provider for calling tools directly via their native protocols
  2. AI Integration: UTCP tools can be exposed to AI agents via ai_mcp_servers with type: utcp entries
  3. Manual Discovery: Supports three manual resolution modes:
    • URL-based (HTTP GET to fetch UTCP manual or OpenAPI spec)
    • File-based (local JSON file with UTCP manual)
    • Inline (call template defined directly in YAML config)
  4. Smart Features: Tool name suffix matching, Liquid template rendering, environment variable resolution, output transforms (Liquid/JS), automatic issue extraction

Key Technical Changes

1. UtcpCheckProvider Class (src/providers/utcp-check-provider.ts)

  • Implements CheckProvider interface with getName() → 'utcp'
  • Static resolveManualCallTemplate() for shared manual resolution (used by both standalone provider and AI bridge)
  • Static callTool() for direct tool invocation (used by MCP SSE server)
  • Instance execute() method with full lifecycle: template rendering, SDK import, client creation, tool calling, transforms, issue extraction
  • Timeout handling with Promise.race pattern
  • Tool name resolution with suffix matching (get_iphttpbin.get_ip)

2. AI Provider Integration (src/providers/ai-check-provider.ts:1478-1578)

  • Extended ai_mcp_servers parsing to extract type: utcp entries
  • UTCP entries are removed from MCP server list and processed separately
  • For each UTCP entry:
    • Resolves manual to call template
    • Creates UTCP client with variables and plugins
    • Discovers all tools via client.getTools()
    • Exposes each discovered tool as an MCP tool to the AI agent
  • Tools are registered in customTools Map with type: 'utcp' and metadata (__utcpManual, __utcpToolName, __utcpVariables, __utcpPlugins)

3. MCP SSE Server Routing (src/providers/mcp-custom-sse-server.ts)

  • Added isUtcpTool() helper to detect UTCP tools
  • Extended tools/list to include UTCP tools alongside regular, workflow, and http_client tools
  • Added executeUtcpTool() method that delegates to UtcpCheckProvider.callTool()
  • Maintains consistent error handling and timeout patterns

4. Shared Issue Normalizer (src/utils/issue-normalizer.ts)

  • Extracted common issue extraction logic from MCP provider
  • extractIssuesFromOutput() handles JSON strings, arrays, objects with issues property, single issue objects
  • normalizeIssue() accepts field aliases: message/text/description, severity/level/priority, file/path/filename, line/startLine/lineNumber
  • Used by both MCP and UTCP providers

5. Provider Registry (src/providers/check-provider-registry.ts)

  • Registers UtcpCheckProvider in try-catch block (graceful degradation if SDK missing)
  • Updates total provider count to 17

Affected System Components

graph TD
    A[Visor Config] --> B[type: utcp check]
    A --> C[type: ai check with ai_mcp_servers]
    
    B --> D[UtcpCheckProvider.execute]
    D --> E[resolveManualCallTemplate]
    E --> F{Manual Type?}
    F -->|URL| G[HTTP Call Template]
    F -->|File| H[File Call Template]
    F -->|Inline| I[Inline Template]
    
    D --> J[UtcpClient.create]
    J --> K[client.callTool]
    K --> L[Transform Output]
    L --> M[Extract Issues]
    
    C --> N[AI Provider]
    N --> O[Parse ai_mcp_servers]
    O --> P{type: utcp?}
    P -->|Yes| Q[Discover UTCP Tools]
    Q --> R[Create CustomToolDefinition]
    R --> S[CustomToolsSSEServer]
    S --> T[AI Agent via MCP]
    
    T --> U[executeUtcpTool]
    U --> V[UtcpCheckProvider.callTool]
    V --> W[Direct Tool Execution]
    
    style D fill:#e1f5ff
    style V fill:#e1f5ff
    style Q fill:#fff4e1
    style U fill:#fff4e1
Loading

Component Relationships

  • UtcpCheckProvider is the core implementation, used in two contexts:

    1. Standalone: type: utcp checks call execute() method
    2. AI Bridge: ai_mcp_servers with type: utcp call static callTool() via SSE server
  • CustomToolsSSEServer acts as the bridge, exposing UTCP tools as MCP tools to AI agents

  • Shared Utilities: issue-normalizer eliminates code duplication between MCP and UTCP providers

Scope Discovery & Context Expansion

Immediate Impact

  • Configuration Schema: Added manual, variables, plugins fields to CheckConfig and CustomToolDefinition
  • Type System: Extended ConfigCheckType union with 'utcp'
  • Documentation: 7 files updated to reflect 17 providers (was 16)

Related Modules

  • Workspace Manager: UTCP tools may need workspace-aware file paths for file-based manuals
  • Environment Resolver: Used for variable substitution in variables field
  • Liquid Extensions: Template rendering in methodArgs and argsTransform
  • Sandbox: JavaScript transforms use secure sandbox execution
  • Telemetry: UTCP calls should emit spans (not implemented in this PR)

Potential Future Work

  • Caching: Tool discovery via getTools() is called on every execution (could cache per manual URL)
  • Async File I/O: fs.readFileSync blocks event loop (could use fs.promises.readFile)
  • Command Provider: Could refactor to use shared issue-normalizer
  • Plugin System: UTCP plugins (@utcp/file, @utcp/text) are optional dependencies

Testing Coverage

31 unit tests for UtcpCheckProvider:

  • Config validation (URL, file, inline manuals)
  • Execution lifecycle (client creation, tool calling, cleanup)
  • Variable resolution via EnvironmentResolver
  • Mock hooks support
  • Issue extraction from structured output
  • Error handling (timeouts, tool not found, SDK failures)
  • Transform application (Liquid and JavaScript)

Integration tests for UTCP-MCP bridge:

  • isUtcpTool detection pattern
  • Manual resolution (URL, inline)
  • Tool discovery and CustomToolDefinition creation
  • Entry extraction from ai_mcp_servers

Registry test: Updated to expect 17 providers (was 16)

References

Core Implementation:

  • src/providers/utcp-check-provider.ts:1-647 - UtcpCheckProvider class
  • src/utils/issue-normalizer.ts:1-162 - Shared issue extraction

AI Integration:

  • src/providers/ai-check-provider.ts:1478-1578 - UTCP tool discovery in ai_mcp_servers
  • src/providers/mcp-custom-sse-server.ts:964-970 - UTCP tool listing
  • src/providers/mcp-custom-sse-server.ts:1145-1165 - UTCP tool execution

Registry & Config:

  • src/providers/check-provider-registry.ts:61-72 - Provider registration
  • src/config.ts:66 - validCheckTypes array
  • src/types/config.ts:221 - ConfigCheckType union

Documentation:

  • docs/utcp-provider.md:1-343 - Complete reference guide
  • examples/utcp-provider-example.yaml:1-290 - 12 usage examples

Tests:

  • tests/unit/providers/utcp-check-provider.test.ts:1-503
  • tests/unit/providers/utcp-mcp-bridge.test.ts:1-307
Metadata
  • Review Effort: 4 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-03-13T18:41:03.892Z | Triggered by: pr_updated | Commit: 03d2af2

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs
Copy link
Contributor

probelabs bot commented Mar 12, 2026

Architecture Issues (9)

Severity Location Issue
🟠 Error src/providers/ai-check-provider.ts:1478
AI provider UTCP integration adds 110 lines of complex logic for tool discovery, client creation, and schema conversion. This duplicates functionality that exists in UtcpCheckProvider and creates tight coupling.
💡 SuggestionExtract UTCP tool discovery into a shared utility module that both UtcpCheckProvider and AICheckProvider can use.
🟠 Error src/providers/utcp-check-provider.ts:590
Timeout implementation uses Promise.race but the timer is only cleared in .finally(). If the promise rejects before the race completes, the timer may not be cleared properly.
💡 SuggestionUse AbortController with a timeout signal instead of Promise.race for proper cleanup.
🟡 Warning src/providers/utcp-check-provider.ts:580
Synchronous file I/O (fs.readFileSync) blocks the event loop. In a workflow system that may execute multiple checks concurrently, this can cause performance degradation.
💡 SuggestionReplace fs.readFileSync with fs.promises.readFile for async file reading.
🟡 Warning src/providers/utcp-check-provider.ts:447
Tool discovery via client.getTools() is called on every execution without caching. For URL-based manuals, this means repeated HTTP requests to fetch the same tool definitions.
💡 SuggestionImplement a cache for tool discovery results keyed by manual URL/file hash with configurable TTL.
🟡 Warning src/providers/mcp-custom-sse-server.ts:1145
executeUtcpTool delegates to UtcpCheckProvider.callTool() static method, creating a dependency from SSE server to UTCP provider.
💡 SuggestionExtract UTCP execution logic into a separate UTCP client wrapper module that both can import.
🟡 Warning src/providers/utcp-check-provider.ts:447
Tool name suffix matching logic is implemented inline in execute(). This is a special case that adds complexity without being generalized.
💡 SuggestionExtract tool name resolution into a separate, testable function. Consider making this behavior configurable.
🟡 Warning src/providers/utcp-check-provider.ts:647
deriveManualName() exists as both instance method and static method. The instance method simply delegates to static method, adding unnecessary indirection.
💡 SuggestionRemove instance methods and use static methods directly, or make instance methods the primary implementation.
🟡 Warning src/providers/utcp-check-provider.ts:580
Path traversal validation logic is complex and duplicated. Similar security validations exist elsewhere in the codebase.
💡 SuggestionExtract path validation logic into a shared security utility for consistent practices.
🟡 Warning src/utils/issue-normalizer.ts:1
Issue normalizer was extracted from MCP provider but command provider still has its own issue extraction logic, creating inconsistency.
💡 SuggestionRefactor command provider to use the shared issue-normalizer utilities for consistency.
\n\n

Architecture Issues (9)

Severity Location Issue
🟠 Error src/providers/ai-check-provider.ts:1478
AI provider UTCP integration adds 110 lines of complex logic for tool discovery, client creation, and schema conversion. This duplicates functionality that exists in UtcpCheckProvider and creates tight coupling.
💡 SuggestionExtract UTCP tool discovery into a shared utility module that both UtcpCheckProvider and AICheckProvider can use.
🟠 Error src/providers/utcp-check-provider.ts:590
Timeout implementation uses Promise.race but the timer is only cleared in .finally(). If the promise rejects before the race completes, the timer may not be cleared properly.
💡 SuggestionUse AbortController with a timeout signal instead of Promise.race for proper cleanup.
🟡 Warning src/providers/utcp-check-provider.ts:580
Synchronous file I/O (fs.readFileSync) blocks the event loop. In a workflow system that may execute multiple checks concurrently, this can cause performance degradation.
💡 SuggestionReplace fs.readFileSync with fs.promises.readFile for async file reading.
🟡 Warning src/providers/utcp-check-provider.ts:447
Tool discovery via client.getTools() is called on every execution without caching. For URL-based manuals, this means repeated HTTP requests to fetch the same tool definitions.
💡 SuggestionImplement a cache for tool discovery results keyed by manual URL/file hash with configurable TTL.
🟡 Warning src/providers/mcp-custom-sse-server.ts:1145
executeUtcpTool delegates to UtcpCheckProvider.callTool() static method, creating a dependency from SSE server to UTCP provider.
💡 SuggestionExtract UTCP execution logic into a separate UTCP client wrapper module that both can import.
🟡 Warning src/providers/utcp-check-provider.ts:447
Tool name suffix matching logic is implemented inline in execute(). This is a special case that adds complexity without being generalized.
💡 SuggestionExtract tool name resolution into a separate, testable function. Consider making this behavior configurable.
🟡 Warning src/providers/utcp-check-provider.ts:647
deriveManualName() exists as both instance method and static method. The instance method simply delegates to static method, adding unnecessary indirection.
💡 SuggestionRemove instance methods and use static methods directly, or make instance methods the primary implementation.
🟡 Warning src/providers/utcp-check-provider.ts:580
Path traversal validation logic is complex and duplicated. Similar security validations exist elsewhere in the codebase.
💡 SuggestionExtract path validation logic into a shared security utility for consistent practices.
🟡 Warning src/utils/issue-normalizer.ts:1
Issue normalizer was extracted from MCP provider but command provider still has its own issue extraction logic, creating inconsistency.
💡 SuggestionRefactor command provider to use the shared issue-normalizer utilities for consistency.
\n\n ### Performance Issues (8)
Severity Location Issue
🟡 Warning src/providers/utcp-check-provider.ts:465
Synchronous file I/O (fs.readFileSync) blocks the event loop. In a high-concurrency environment with multiple UTCP checks executing, this can cause performance degradation. The file read should use fs.promises.readFile for async operation.
💡 SuggestionReplace fs.readFileSync with fs.promises.readFile and make resolveManualCallTemplate async. Update all callers to await the promise.
🔧 Suggested Fix
content = await fs.promises.readFile(resolvedPath, 'utf-8');
🟡 Warning src/providers/utcp-check-provider.ts:485
Synchronous realpathSync (fs.realpathSync) blocks the event loop during symlink validation. This should use fs.promises.realpath for async operation.
💡 SuggestionReplace fs.realpathSync with fs.promises.realpath and make resolveManualCallTemplate async.
🔧 Suggested Fix
const realPath = await fs.promises.realpath(resolvedPath);
🟡 Warning src/providers/ai-check-provider.ts:1478
UTCP tool discovery happens serially in a forEach loop. Each UTCP entry requires SDK import, plugin loading, client creation, and tool discovery - all blocking operations. With multiple UTCP entries, this creates O(n) sequential delay instead of parallel O(1) with proper concurrency.
💡 SuggestionReplace forEach with Promise.all or Promise.allSettled to discover UTCP tools in parallel. Each entry's discovery is independent and can run concurrently.
🔧 Suggested Fix
if (utcpEntriesFromMcp.length > 0) {
  const discoveryResults = await Promise.allSettled(
    utcpEntriesFromMcp.map(async (entry) => {
      // ... discovery logic for each entry
    })
  );
  // Process results and handle failures
}
🟡 Warning src/providers/utcp-check-provider.ts:335
UTCP plugins are loaded serially in a for loop with dynamic imports. Each await import() blocks the next. While plugin loading is typically fast, this pattern is suboptimal for multiple plugins.
💡 SuggestionUse Promise.allSettled to load plugins in parallel, handling failures gracefully.
🔧 Suggested Fix
const plugins = cfg.plugins || ['http'];
await Promise.allSettled(
  plugins.map(plugin => import(`@utcp/${plugin}`))
);
🟡 Warning src/providers/ai-check-provider.ts:1504
UTCP tool discovery via client.getTools() is called on every AI check execution. For long-running AI agents or multiple checks with the same UTCP manual, this results in redundant network/disk I/O to fetch and parse the manual. The discovered tools don't change during execution.
💡 SuggestionImplement a cache for discovered tools keyed by manual URL/hash. Cache could be in-memory (Map) with TTL, or use the existing sessionInfo for cache scope. This would reduce repeated manual fetches.
🔧 Suggested Fix
// Add cache at class or module level
private static toolCache = new Map<string, {tools: any[], timestamp: number}>();
private static CACHE_TTL = 5 * 60 * 1000; // 5 minutes

// In discovery logic:
const cacheKey = typeof manual === 'string' ? manual : JSON.stringify(manual);
const cached = this.toolCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
tools = cached.tools;
} else {
tools = await client.getTools();
this.toolCache.set(cacheKey, {tools, timestamp: Date.now()});
}

🟡 Warning src/providers/utcp-check-provider.ts:335
UTCP plugins are imported on every execute() call. Dynamic imports are cached by Node.js, but the try-catch and logging overhead is repeated unnecessarily. The plugin availability check could be done once at provider initialization.
💡 SuggestionCheck plugin availability once during provider initialization or first use, cache the result, and skip repeated import attempts.
🔧 Suggested Fix
// Add class property to track loaded plugins
private loadedPlugins = new Set<string>();

// In execute:
for (const plugin of plugins) {
if (!this.loadedPlugins.has(plugin)) {
try {
await import(@utcp/${plugin});
this.loadedPlugins.add(plugin);
} catch (err) {
logger.debug(UTCP plugin @utcp/${plugin} not available: ${err});
}
}
}

🟡 Warning src/providers/utcp-check-provider.ts:355
Tool name resolution calls client.getTools() on every execution, which may involve network/disk I/O to fetch the manual. For frequently called tools with known names, this is unnecessary overhead. The suffix matching logic could be optimized.
💡 SuggestionCache discovered tools per manual URL/hash to avoid repeated getTools() calls. Or allow users to specify fully qualified tool names to skip resolution entirely.
🔧 Suggested Fix
// Add caching for discovered tools
private static toolsCache = new Map<string, {tools: string[], timestamp: number}>();

// In execute:
const cacheKey = JSON.stringify(callTemplate);
let toolNames = this.toolsCache.get(cacheKey)?.tools;
if (!toolNames || Date.now() - this.toolsCache.get(cacheKey)!.timestamp > 60000) {
const tools = await client.getTools();
toolNames = tools.map((t: any) => t.name as string);
this.toolsCache.set(cacheKey, {tools: toolNames, timestamp: Date.now()});
}

🟡 Warning src/providers/utcp-check-provider.ts:364
The Promise.race timeout pattern has a subtle issue: if the tool call completes successfully exactly when the timeout fires, both promises resolve. The .finally() clears the timer, but there's a race condition window where the timeout callback could fire after being cleared. More critically, if client.callTool throws synchronously (unlikely but possible), the timer is never cleared.
💡 SuggestionUse AbortController with a timeout signal instead of Promise.race. This is the modern, cancellable approach for HTTP/timeouts.
🔧 Suggested Fix
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
  const result = await client.callTool(toolName, methodArgs as Record<string, any>, {
    signal: controller.signal
  });
  clearTimeout(timer);
  return result;
} catch (error) {
  if (error.name === 'AbortError') {
    throw new Error(`UTCP tool call timed out after ${cfg.timeout || 60}s`);
  }
  throw error;
}

Quality Issues (16)

Severity Location Issue
🟡 Warning src/providers/utcp-check-provider.ts:390
Using fs.readFileSync blocks the event loop. For a production system handling concurrent requests, this can cause performance degradation. Consider using fs.promises.readFile for async file I/O.
💡 SuggestionReplace fs.readFileSync with fs.promises.readFile and make resolveManualCallTemplate async. This is especially important for file-based manual resolution which may be called frequently.
🟡 Warning src/providers/utcp-check-provider.ts:448
The finally block swallows errors from client.close() silently. While this prevents cleanup errors from masking original errors, it could leave resources open if close() fails repeatedly.
💡 SuggestionConsider logging close() failures instead of silently ignoring them: catch (err) { logger.warn(`Failed to close UTCP client: ${err}`); }
🟡 Warning src/providers/utcp-check-provider.ts:448
The Promise.race timeout pattern clears the timer in .finally(), but if the promise rejects before the race completes, the timer might not be cleared properly, causing a potential memory leak.
💡 SuggestionStore the timer reference and ensure it's cleared in both resolve and reject paths, or use AbortController with fetch if the UTCP SDK supports it.
🟡 Warning src/providers/utcp-check-provider.ts:448
Default timeout of 60000ms (60 seconds) is a magic number without clear justification. Different tool types may need different timeouts.
💡 SuggestionDefine a named constant like DEFAULT_UTCP_TIMEOUT_MS = 60000 with a comment explaining why this value was chosen, or make it configurable via environment variable.
🟡 Warning src/providers/utcp-check-provider.ts:448
Timeout error message includes the timeout value but doesn't indicate which tool timed out or what manual was being used, making debugging difficult.
💡 SuggestionInclude more context in timeout error: `UTCP tool '${toolName}' from manual '${typeof manual === 'string' ? manual : 'inline'}' timed out after ${timeoutMs}ms`
🟡 Warning src/providers/ai-check-provider.ts:1478
The UTCP tool discovery logic in ai_mcp_servers (lines 1478-1566) adds 88 lines of complex code to an already large method. This violates Single Responsibility Principle and makes the AI provider harder to maintain.
💡 SuggestionExtract UTCP tool discovery into a separate private method like 'discoverUtcpTools(entry: UtcpEntry): Promise<CustomToolDefinition[]>' to improve readability and testability.
🟡 Warning src/providers/ai-check-provider.ts:1478
UTCP tool discovery errors are caught and logged but don't prevent the AI check from running. This could lead to partial tool availability without the user knowing some tools failed to load.
💡 SuggestionConsider collecting all discovery errors and either: (1) failing fast if any UTCP entry fails, or (2) returning a summary of loaded vs failed tools to the user.
🟡 Warning src/providers/mcp-custom-sse-server.ts:1145
executeUtcpTool delegates to UtcpCheckProvider.callTool() but doesn't pass through session context or debug logging consistently. This makes debugging harder when tools are called via AI vs directly.
💡 SuggestionEnsure executeUtcpTool logs with the same prefix and detail level as executeHttpClientTool for consistent observability.
🟡 Warning tests/unit/providers/utcp-check-provider.test.ts:1
Tests cover happy paths and basic error cases, but missing negative tests for: (1) malformed JSON in manual files, (2) network errors during URL-based manual discovery, (3) concurrent calls to the same manual, (4) invalid Liquid template syntax in methodArgs.
💡 SuggestionAdd test cases for these edge cases to ensure robustness. Malformed JSON should produce clear error messages, network errors should be handled gracefully, and concurrent calls should not interfere with each other.
🟡 Warning tests/unit/providers/utcp-check-provider.test.ts:1
Unit tests mock the UTCP SDK completely, which is good for isolation but doesn't catch integration issues with the actual SDK. The PR description mentions real integration tests but they're not in the test files.
💡 SuggestionAdd integration tests that actually call the UTCP SDK with a test server (e.g., using nock or a local HTTP server) to verify the integration works end-to-end.
🟡 Warning src/providers/utcp-check-provider.ts:390
Tool discovery via client.getTools() is called on every execution (line 448). For frequently used tools, this is inefficient as the tool list doesn't change.
💡 SuggestionImplement a cache keyed by manual URL/content to store discovered tools. Use a TTL (e.g., 5 minutes) to balance freshness and performance. This would reduce SDK calls for repeated executions.
🟡 Warning src/providers/utcp-check-provider.ts:390
Inline manual validation only checks for call_template_type but doesn't validate that required fields like 'url' for HTTP templates are present. This will fail at runtime with a cryptic error.
💡 SuggestionAdd validation for call-template-specific fields: if call_template_type === 'http', require 'url' and 'http_method'. Provide clear error messages at config validation time.
🟡 Warning tests/unit/providers/utcp-mcp-bridge.test.ts:1
UTCP-MCP bridge tests mock the UTCP SDK but don't test the actual integration between CustomToolsSSEServer and UtcpCheckProvider.callTool(). The test for executeUtcpTool is missing.
💡 SuggestionAdd a test that verifies CustomToolsSSEServer correctly routes UTCP tool calls to UtcpCheckProvider.callTool() with the correct parameters (manual, toolName, args, options).
🟡 Warning src/providers/check-provider-registry.ts:61
UtcpCheckProvider registration is wrapped in try-catch that logs to console.error. This is inconsistent with how ClaudeCodeCheckProvider is registered (no try-catch).
💡 SuggestionEither wrap both optional providers in try-catch for consistency, or neither. The current inconsistency suggests different failure handling strategies without clear justification.
🟡 Warning src/providers/utcp-check-provider.ts:448
isTimeoutError() checks for 'timeout' in message string, which is fragile. Different error types or SDK versions might use different wording.
💡 SuggestionCheck error.code for specific timeout codes (e.g., ETIMEDOUT, TIMEOUT) in addition to message string matching. This is more robust against SDK changes.
🟡 Warning tests/unit/providers/check-provider-registry.test.ts:173
Test expects exactly 19 providers (17 default + 2 custom). This is a magic number that will break every time a provider is added, making maintenance costly.
💡 SuggestionInstead of hardcoding the count, calculate it dynamically: const DEFAULT_PROVIDER_COUNT = ['ai', 'command', 'script', 'http', 'http_input', 'http_client', 'noop', 'log', 'memory', 'github', 'claude-code', 'mcp', 'human-input', 'workflow', 'git-checkout', 'a2a', 'utcp'].length; then use DEFAULT_PROVIDER_COUNT + customProviders.length

Powered by Visor from Probelabs

Last updated: 2026-03-13T18:40:08.960Z | Triggered by: pr_updated | Commit: 03d2af2

💡 TIP: You can chat with Visor using /visor ask <your question>

Test and others added 6 commits March 12, 2026 23:11
UTCP tools can now be exposed as MCP tools to AI agents via
ai_mcp_servers with type: utcp entries. Tools are discovered from
the UTCP manual at setup time and routed through the UTCP SDK at
call time via the CustomToolsSSEServer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared extractIssuesFromOutput/normalizeIssue to
  src/utils/issue-normalizer.ts, used by MCP and UTCP providers
- Add UtcpCheckProvider.callTool() static method for shared UTCP
  client lifecycle (import, create, call, close)
- Make executeUtcpTool in SSE server delegate to callTool()
- Add file existence check and JSON parse error handling in
  resolveManualCallTemplate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e wrapping

The --fail-fast description wraps across lines at 80 columns, causing
toContain to fail. Normalize whitespace before asserting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clear timeout timer in Promise.race .finally() to prevent resource
  leaks when the tool call succeeds before the timeout fires
- Throw from transform error handlers instead of returning early, so
  the outer finally block properly closes the UTCP client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reject null bytes in file paths
- Validate resolved path stays within cwd using normalized prefix check
- Resolve symlinks and re-validate to prevent symlink-based traversal
- URL and inline manuals are unaffected (handled by SDK, not read locally)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The instance execute() method now delegates to the static callTool()
for SDK import, plugin loading, client creation, tool calling with
timeout, and cleanup. This eliminates the duplicated lifecycle logic
and makes callTool the single entry point for both the standalone
provider and the MCP-bridge SSE server.

Also moved tool name suffix matching into callTool so it works for
both code paths, and removed dead instance wrapper methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant