Skip to content

feat(cli): expose runTool / runSystemTool as library functions#744

Merged
IvanMurzak merged 7 commits intomainfrom
feature/lib-run-tool
May 10, 2026
Merged

feat(cli): expose runTool / runSystemTool as library functions#744
IvanMurzak merged 7 commits intomainfrom
feature/lib-run-tool

Conversation

@IvanMurzak
Copy link
Copy Markdown
Owner

@IvanMurzak IvanMurzak commented May 10, 2026

Summary

  • Adds runTool and runSystemTool library functions that mirror the existing CLI commands (unity-mcp-cli run-tool, unity-mcp-cli run-system-tool).
  • Refactors the CLI commands to delegate to the new lib functions — single implementation, identical behaviour.
  • Same connection-resolution priority as the CLI (explicit override → project config → deterministic localhost port), structured { kind: 'success' | 'failure', ... } results, no console output past the lib boundary, no thrown exceptions.
import { runTool, runSystemTool } from 'unity-mcp-cli';

const result = await runTool({
  toolName: 'tool-list',
  unityProjectPath: '/path/to/Unity/project',
  input: { regexSearch: 'gameobject', includeDescription: true },
});
if (result.kind === 'success') {
  // result.data === parsed JSON body from the Unity plugin
}

Test plan

  • npm test -- lib-run-tool — 23 cases covering happy path, validation failures, HTTP error classes, timeouts, connection refused, invalid JSON, plus regression coverage of the connection-resolution layering.
  • npm test — full suite (370 cases) green after merging main.
  • Manual: unity-mcp-cli run-tool tool-list --unity-project-path … against a running Unity editor matches the pre-refactor output.

🤖 Generated with Claude Code

IvanMurzak added 3 commits May 9, 2026 02:44
- Added runTool and runSystemTool functions to handle invoking MCP tools via the Unity plugin's HTTP API.
- Introduced structured error handling for various failure scenarios including HTTP errors, timeouts, and connection issues.
- Updated CLI command to utilize the new library functions, improving separation of concerns and code maintainability.
- Enhanced type definitions for tool invocation options and results, ensuring better type safety.
- Created comprehensive tests for both successful invocations and various failure modes, ensuring robust functionality.
…sions

- Deleted obsolete DLLs: System.Runtime.CompilerServices.Unsafe.6.1.2.dll, System.Text.Encoding.CodePages.7.0.0.dll, System.Text.Encodings.Web.8.0.0.dll, System.Text.Json.8.0.5.dll, System.Threading.Channels.8.0.0.dll, System.Threading.Tasks.Extensions.4.5.4.dll.
- Added updated DLLs: System.Runtime.CompilerServices.Unsafe.dll, System.Text.Encoding.CodePages.dll, System.Text.Encodings.Web.dll, System.Text.Json.dll, System.Threading.Channels.dll, System.Threading.Tasks.Extensions.dll.
- Updated corresponding .meta files for new DLLs.
Copilot AI review requested due to automatic review settings May 10, 2026 02:27
@IvanMurzak IvanMurzak self-assigned this May 10, 2026
@IvanMurzak IvanMurzak added the enhancement New feature or request label May 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR promotes the existing run-tool / run-system-tool CLI functionality into the unity-mcp-cli library API by introducing runTool() and runSystemTool() exports, then refactors the CLI commands to delegate the HTTP invocation to those new library functions.

Changes:

  • Added runTool / runSystemTool library functions with structured { kind: 'success' | 'failure' } results and a comprehensive new test suite.
  • Refactored unity-mcp-cli run-tool and run-system-tool commands to call the new library functions instead of implementing fetch logic inline.
  • Updated CLI changelog to document the new library surface area.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
Unity-Tests/6000.6.0a2/Assets/Plugins/NuGet/McpPlugin.dll.meta Updates the Unity asset GUID for the test DLL plugin import metadata.
Unity-Tests/6000.6.0a2/Assets/Plugins/NuGet/McpPlugin.Common.dll.meta Updates the Unity asset GUID for the test DLL plugin import metadata.
cli/tests/lib-run-tool.test.ts Adds Vitest coverage for runTool / runSystemTool success paths, validation, transport failures, and connection resolution.
cli/src/lib/types.ts Introduces new public types for run-tool results/options and failure reasons.
cli/src/lib/run-tool.ts Adds library-safe implementations of runTool / runSystemTool, including URL/token resolution and error classification.
cli/src/lib.ts Exposes runTool / runSystemTool and their types as top-level library exports.
cli/src/commands/run-tool.ts Refactors the CLI command to delegate to runTool and maps structured failures back to CLI output/exit behavior.
cli/src/commands/run-system-tool.ts Refactors the CLI command to delegate to runSystemTool and maps structured failures back to CLI output/exit behavior.
cli/package.json Fixes JSON formatting (no functional change).
cli/CHANGELOG.md Documents the new runTool / runSystemTool library exports and the internal CLI refactor.

Comment thread cli/src/lib/run-tool.ts Outdated
Comment on lines +134 to +140
if (
(opts.url === undefined || opts.url.length === 0) &&
(typeof opts.unityProjectPath !== 'string' || opts.unityProjectPath.trim().length === 0)
) {
return makeFailure({
endpoint: '',
reason: 'invalid-input',
Comment thread cli/src/lib/run-tool.ts Outdated
): { kind: 'success'; url: string; token: string | undefined } | RunToolFailure {
if (opts.url) {
const url = opts.url.replace(/\/$/, '');
return { kind: 'success', url, token: opts.token };
Comment thread cli/src/lib/run-tool.ts Outdated
Comment on lines +175 to +179
const config = exists ? readConfig(projectPath) : null;
const fromConfig = config
? resolveConnectionFromConfig(config)
: { url: undefined, token: undefined };

Comment thread cli/src/lib/run-tool.ts Outdated
Comment on lines +155 to +167
// Validate the project path. Library-safe variant of the CLI's
// `resolveAndValidateProjectPath` — returns a structured failure
// instead of calling `process.exit` when the path is missing.
const validated = requireProjectPath(opts.unityProjectPath);
if (!validated.ok) {
return makeFailure({
endpoint: '',
reason: 'invalid-input',
message: validated.error.message,
error: validated.error,
});
}
const projectPath = validated.projectPath;
Comment thread cli/src/lib/types.ts
Comment on lines +466 to +467
* the body is `{}`. Anything other than `undefined` / `null` /
* `object` is rejected with a `kind: 'failure'` result.
Comment thread cli/src/commands/run-tool.ts Outdated
Comment on lines +77 to +99
if (result.kind === 'success') {
spinner?.success(`${toolName} completed`);

const responseText = stringifyForRaw(result.data);
if (options.raw) {
process.stdout.write(responseText);
} else {
ui.success('Response:');
console.log(typeof responseData === 'string'
? responseData
: JSON.stringify(responseData, null, 2));
console.log(typeof result.data === 'string'
? result.data
: JSON.stringify(result.data, null, 2));
}
} catch (err) {
spinner?.stop();
const isTimeout = err instanceof Error && err.name === 'AbortError';
const cause = err instanceof Error && 'cause' in err ? (err.cause as Error & { code?: string }) : null;
const causeCode = cause?.code ?? '';
const rootMessage = cause?.message || causeCode || (err instanceof Error ? err.message : String(err));
const errorSignature = `${rootMessage} ${causeCode}`;
const isConnectionRefused = errorSignature.includes('ECONNREFUSED');
const isConnectionReset = errorSignature.includes('ECONNRESET');
const isNetworkError = errorSignature.includes('EAI_AGAIN') || errorSignature.includes('ENOTFOUND');
return;
}

let displayMessage: string;
if (isTimeout) {
displayMessage = `Tool call timed out after ${timeoutMs / 1000} seconds: ${toolName}`;
} else if (isConnectionRefused) {
displayMessage = `Connection refused at ${endpoint}. Is the MCP server running? Start Unity Editor with the MCP plugin first.`;
} else if (isConnectionReset) {
displayMessage = `Connection was reset by the server at ${endpoint}. The server may have crashed or restarted.`;
} else if (isNetworkError) {
displayMessage = `Cannot reach ${endpoint}. Check your network connection and server URL.`;
} else {
displayMessage = `${rootMessage}`;
if (cause && cause.message !== rootMessage) {
displayMessage += ` (${cause.message})`;
}
}
spinner?.stop();
handleFailure(result, { toolName, endpoint, raw: options.raw, timeoutMs });
});

if (options.raw) {
process.stderr.write(displayMessage + '\n');
function stringifyForRaw(data: unknown): string {
if (typeof data === 'string') return data;
if (data === undefined) return '';
return JSON.stringify(data);
}
Comment thread cli/src/commands/run-system-tool.ts Outdated
Comment on lines 102 to 114
function handleFailure(failure: RunToolFailure, ctx: FailureContext): never {
switch (failure.reason) {
case 'http-error': {
if (ctx.raw) {
process.stdout.write(stringifyForRaw(failure.data));
} else {
ui.error(`Failed to call system tool: ${displayMessage}`);
ui.error(`HTTP ${failure.httpStatus}: ${failure.message}`);
if (failure.data !== undefined) {
ui.info(typeof failure.data === 'string'
? failure.data
: JSON.stringify(failure.data, null, 2));
}
}
Comment thread cli/src/commands/run-system-tool.ts Outdated
Comment on lines +71 to +93
if (result.kind === 'success') {
spinner?.success(`${toolName} completed`);

const responseText = stringifyForRaw(result.data);
if (options.raw) {
process.stdout.write(responseText);
} else {
ui.success('Response:');
console.log(typeof responseData === 'string'
? responseData
: JSON.stringify(responseData, null, 2));
console.log(typeof result.data === 'string'
? result.data
: JSON.stringify(result.data, null, 2));
}
} catch (err) {
spinner?.stop();
const isTimeout = err instanceof Error && err.name === 'AbortError';
const cause = err instanceof Error && 'cause' in err ? (err.cause as Error & { code?: string }) : null;
const causeCode = cause?.code ?? '';
const rootMessage = cause?.message || causeCode || (err instanceof Error ? err.message : String(err));
const errorSignature = `${rootMessage} ${causeCode}`;
const isConnectionRefused = errorSignature.includes('ECONNREFUSED');
const isConnectionReset = errorSignature.includes('ECONNRESET');
const isNetworkError = errorSignature.includes('EAI_AGAIN') || errorSignature.includes('ENOTFOUND');
return;
}

let displayMessage: string;
if (isTimeout) {
displayMessage = `System tool call timed out after ${timeoutMs / 1000} seconds: ${toolName}`;
} else if (isConnectionRefused) {
displayMessage = `Connection refused at ${endpoint}. Is the MCP server running? Start Unity Editor with the MCP plugin first.`;
} else if (isConnectionReset) {
displayMessage = `Connection was reset by the server at ${endpoint}. The server may have crashed or restarted.`;
} else if (isNetworkError) {
displayMessage = `Cannot reach ${endpoint}. Check your network connection and server URL.`;
} else {
displayMessage = `${rootMessage}`;
if (cause && cause.message !== rootMessage) {
displayMessage += ` (${cause.message})`;
}
}
spinner?.stop();
handleFailure(result, { toolName, endpoint, raw: options.raw, timeoutMs });
});

if (options.raw) {
process.stderr.write(displayMessage + '\n');
function stringifyForRaw(data: unknown): string {
if (typeof data === 'string') return data;
if (data === undefined) return '';
return JSON.stringify(data);
}
Comment on lines 1 to 3
fileFormatVersion: 2
guid: 8ca0a15d310a5f843b0937012d11f4ea
guid: f40aae41f95ca764c8729414e1abe315
PluginImporter:
Comment on lines 1 to 3
fileFormatVersion: 2
guid: f2b17cb86ebbab0498de7a05697c50c4
guid: c061112e81f53a24fb5437ec4fe122c5
PluginImporter:
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 10, 2026

Test Results

   12 files    546 suites   43m 58s ⏱️
  919 tests   918 ✅ 1 💤 0 ❌
5 514 runs  5 508 ✅ 6 💤 0 ❌

Results for commit 2981e57.

♻️ This comment has been updated with latest results.

Copilot AI review requested due to automatic review settings May 10, 2026 03:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Comment thread cli/src/lib/run-tool.ts Outdated
Comment on lines +133 to +141
if (
(opts.url === undefined || opts.url.length === 0) &&
(typeof opts.unityProjectPath !== 'string' || opts.unityProjectPath.trim().length === 0)
) {
return makeFailure({
endpoint: '',
reason: 'invalid-input',
message: 'Either unityProjectPath or url must be provided.',
});
Comment thread cli/src/lib/run-tool.ts
const validationFailure = validateOptions(opts);
if (validationFailure) return validationFailure;

const resolved = resolveConnection(opts);
Comment thread cli/src/lib/types.ts
Comment on lines +466 to +467
* the body is `{}`. Anything other than `undefined` / `null` /
* `object` is rejected with a `kind: 'failure'` result.
Comment thread cli/src/lib/run-tool.ts
Comment on lines +246 to +253
const error = err instanceof Error ? err : new Error(String(err));
const causeCode = getCause(err)?.code;

let reason: RunToolFailureReason = 'unknown';
if (causeCode === 'ECONNREFUSED') reason = 'connection-refused';
else if (causeCode === 'ECONNRESET') reason = 'connection-reset';
else if (causeCode === 'ENOTFOUND' || causeCode === 'EAI_AGAIN') reason = 'network-error';

IvanMurzak added 2 commits May 9, 2026 20:11
- Validate --timeout before parseInput so a bad value short-circuits
  before reading --input-file or hitting the network.
- Compute authSource once and reuse for both verbose and ui.label
  outputs (was duplicated).
- Drop dead `?? '60000'` fallback (commander already defaults the
  option's value).
- Tighten lib validateOptions: typeof-string check on opts.url
  matches the existing opts.unityProjectPath shape, removing an
  asymmetric defensive gap.

simplify-pass: 1
Copilot AI review requested due to automatic review settings May 10, 2026 08:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Comment thread cli/src/lib/run-tool.ts
}
const projectPath = validated.projectPath;

const config = readConfig(projectPath);
Comment on lines +102 to +106
if (result.kind === 'success') {
spinner?.success(`${toolName} completed`);
if (options.raw) {
process.stdout.write(stringifyForRaw(result.data));
} else {
Comment on lines +132 to +136
function handleFailure(failure: RunToolFailure, ctx: FailureContext): never {
if (failure.reason === 'http-error') {
if (ctx.raw) {
process.stdout.write(stringifyForRaw(failure.data));
} else {
Comment thread cli/src/lib/types.ts
Comment on lines +466 to +467
* the body is `{}`. Anything other than `undefined` / `null` /
* `object` is rejected with a `kind: 'failure'` result.
@IvanMurzak IvanMurzak merged commit 64189c0 into main May 10, 2026
21 checks passed
@IvanMurzak IvanMurzak deleted the feature/lib-run-tool branch May 10, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants