diff --git a/.gitignore b/.gitignore index e3d220f9d6..b825021816 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,7 @@ docs/superpowers/ # Reference DLLs fetched by tools/compile-check.sh (not redistributable) .compile-refs/ +.compile-out/ + +# Local-only scratch files marked by name +*.DO-NOT-COMMIT.* diff --git a/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs new file mode 100644 index 0000000000..f03161e823 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Services; +using UnityEditor; + +namespace MCPForUnity.Editor.Clients.Configurators +{ + /// + /// DeepSeek Harness (dsh) has no client config file. MCP servers are mounted + /// through its plugin manager as a configuration-only bundle (a manifest plus a + /// cordis.patch.yml row for @deepseek-ai/dsh-mcp-client), so setup is + /// manual like the other UI-driven clients: the snippet below holds the two bundle + /// files and the dsh plugin add command that installs them. After a DSH + /// reload the tools surface as mcp__unityMCP__<tool>. From there DSH's + /// own plugin manager owns enable/disable and removal. + /// + public class DeepSeekHarnessConfigurator : McpClientConfiguratorBase + { + public const string ClientName = "DeepSeek Harness"; + + private const string ServerName = "unityMCP"; + private const string BundleName = "@local/unity-mcp-dsh"; + + public DeepSeekHarnessConfigurator() : base(new McpClient + { + name = ClientName, + windowsConfigPath = BundlePatchPath(), + macConfigPath = BundlePatchPath(), + linuxConfigPath = BundlePatchPath() + }) + { } + + public override bool SupportsAutoConfigure => false; + + /// DSH is installed once it has run at least once (its home directory exists). + public override bool IsInstalled => Directory.Exists(DshHome()); + + public override string GetConfigPath() => BundlePatchPath(); + + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + client.SetStatus(McpStatus.NotConfigured, "DeepSeek Harness setup is manual; DSH's plugin manager owns the bundle."); + return client.status; + } + + public override void Configure() + { + throw new InvalidOperationException( + "DeepSeek Harness setup is manual. Use the Manual Configuration snippet below: " + + "it holds the two bundle files and the 'dsh plugin add' command to run."); + } + + public override IList GetInstallationSteps() => new List + { + "Install DeepSeek Harness and run any 'dsh' command once (creates ~/.dsh)", + "Create the two files shown in the Manual Configuration snippet below", + "Quit DeepSeek Harness, run the 'dsh plugin add' command from the snippet, then reopen it", + "Restart DeepSeek Harness; Unity tools appear as mcp__unityMCP__", + "DSH's plugin manager can enable/disable or remove the bundle afterwards" + }; + + public override string GetManualSnippet() + { + try + { + string bundleDir = BundleDir(); + string profile = ProfileName(); + var sb = new StringBuilder(); + sb.AppendLine("# DeepSeek Harness setup. $DSH_HOME defaults to ~/.dsh."); + sb.AppendLine("# 'desktop' is the Desktop app's own profile: quit DeepSeek Harness, run the"); + sb.AppendLine("# commands below, then reopen it. With a standalone dsh (npm install), pick"); + sb.AppendLine("# another profile name instead; 'desktop' is reserved for the Desktop app."); + sb.AppendLine(); + sb.AppendLine("# --- " + bundleDir + "/package.json ---"); + sb.AppendLine("{"); + sb.AppendLine(" \"name\": \"@local/unity-mcp-dsh\","); + sb.AppendLine(" \"version\": \"1.0.0\","); + sb.AppendLine(" \"private\": true,"); + sb.AppendLine(" \"type\": \"module\","); + sb.AppendLine(" \"dsh\": { \"bundle\": { \"patch\": \"./cordis.patch.yml\" } }"); + sb.AppendLine("}"); + sb.AppendLine(); + sb.AppendLine("# --- " + bundleDir + "/cordis.patch.yml ---"); + sb.Append(Row()); + sb.AppendLine(); + sb.AppendLine("# Install:"); + sb.AppendLine("# dsh plugin --profile " + ShellQuote(profile) + " add " + ShellQuote(bundleDir)); + sb.AppendLine("# Remove:"); + sb.AppendLine("# dsh plugin --profile " + ShellQuote(profile) + " remove " + ShellQuote(BundleName)); + return sb.ToString(); + } + catch (Exception ex) + { + return "# error: " + ex.Message; + } + } + + /// Renders the patch row for the transport currently selected in Unity. + private static string Row() + { + bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; + var sb = new StringBuilder(); + sb.AppendLine("# >>> MCP for Unity (unityMCP) managed block >>>"); + sb.AppendLine("- insert:"); + sb.AppendLine(" - id: unitymcp"); + sb.AppendLine(" name: '@deepseek-ai/dsh-mcp-client'"); + sb.AppendLine(" config:"); + sb.AppendLine(" serverName: " + ServerName); + + if (useHttp) + { + sb.AppendLine(" transport: streamable-http"); + sb.AppendLine(" url: " + YamlQuote(HttpEndpointUtility.GetMcpRpcUrl())); + if (HttpEndpointUtility.IsRemoteScope()) + { + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!string.IsNullOrEmpty(apiKey)) + { + sb.AppendLine(" headers:"); + sb.AppendLine(" " + AuthConstants.ApiKeyHeader + ": " + YamlQuote(apiKey)); + } + } + } + else + { + var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts(); + if (string.IsNullOrWhiteSpace(uvxPath)) + { + throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings."); + } + + sb.AppendLine(" transport: stdio"); + sb.AppendLine(" command: " + YamlQuote(uvxPath)); + sb.AppendLine(" args:"); + foreach (string flag in AssetPathUtility.GetUvxDevFlagsList()) + { + sb.AppendLine(" - " + YamlQuote(flag)); + } + foreach (string arg in AssetPathUtility.GetBetaServerFromArgsList()) + { + sb.AppendLine(" - " + YamlQuote(arg)); + } + sb.AppendLine(" - " + YamlQuote(packageName)); + sb.AppendLine(" - " + YamlQuote("--transport")); + sb.AppendLine(" - " + YamlQuote("stdio")); + if (MCPServiceLocator.Platform.IsWindows()) + { + sb.AppendLine(" env:"); + sb.AppendLine(" SystemRoot: " + YamlQuote(MCPServiceLocator.Platform.GetSystemRoot())); + } + } + + sb.AppendLine(" failOnStartupError: true"); + sb.AppendLine("# <<< MCP for Unity (unityMCP) <<<"); + return sb.ToString(); + } + + /// DSH home: $DSH_HOME when set, else ~/.dsh. + private static string DshHome() + { + string envHome = Environment.GetEnvironmentVariable("DSH_HOME"); + if (!string.IsNullOrEmpty(envHome)) return envHome; + + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return string.IsNullOrEmpty(home) ? string.Empty : Path.Combine(home, ".dsh"); + } + + /// Target profile: DSH_PROFILE when set, else desktop. + private static string ProfileName() + { + string envProfile = Environment.GetEnvironmentVariable("DSH_PROFILE"); + return string.IsNullOrEmpty(envProfile) ? "desktop" : envProfile; + } + + private static string BundleDir() => Path.Combine(DshHome(), "bundles", ProfileName(), "unity-mcp"); + + private static string BundlePatchPath() => Path.Combine(BundleDir(), "cordis.patch.yml"); + + /// Single-quotes a YAML scalar; embedded single quotes are doubled. + private static string YamlQuote(string value) => "'" + (value ?? string.Empty).Replace("'", "''") + "'"; + + /// Double-quotes a shell argument so cmd.exe metacharacters stay literal. + private static string ShellQuote(string value) => "\"" + (value ?? string.Empty).Replace("\"", "\\\"") + "\""; + } +} diff --git a/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs.meta new file mode 100644 index 0000000000..2e9188f8d9 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/DeepSeekHarnessConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f9d6c1e8a3b4d7f9e5a1c2b3d4e5f60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/website/docs/getting-started/clients.md b/website/docs/getting-started/clients.md index 245d17a25d..83a34f9b98 100644 --- a/website/docs/getting-started/clients.md +++ b/website/docs/getting-started/clients.md @@ -27,13 +27,14 @@ MCP for Unity auto-configures every client the package detects on your machine. | **OpenClaw** | HTTP / stdio | yes | yes | yes | Requires `openclaw-mcp-bridge` plugin enabled. Follows MCP for Unity's transport choice. | | **Antigravity** | HTTP | yes | yes | varies | Requires an MCP toggle in Antigravity settings. | | **Pi** | stdio / HTTP | yes | yes | yes | Needs an MCP extension (Pi ships no MCP client of its own): `pi install npm:pi-mcp-adapter`. Writes the shared `~/.config/mcp/mcp.json`. | +| **DeepSeek Harness** | HTTP / stdio | no (manual Configure) | yes | yes | Manual setup: the snippet holds a configuration-only DSH bundle (`$DSH_HOME/bundles//unity-mcp/`) installed with `dsh plugin add`. Tools appear as `mcp__unityMCP__`. | ## How to pick - **You want it to just work**: Claude Desktop. Stdio means no port conflicts and no firewall prompts. - **You're building a multi-agent or remote workflow**: anything HTTP. Multiple clients can share one Python server; see [Multi-Instance Routing](/guides/multi-instance). - **You're already in your IDE**: Cursor, VS Code Copilot, or Cline — keeps you in flow. -- **You want a terminal**: Claude Code, Copilot CLI, Codex, Gemini CLI, Qwen Code, or Pi. +- **You want a terminal**: Claude Code, Copilot CLI, Codex, Gemini CLI, Qwen Code, Pi, or DeepSeek Harness. ## Manual configuration diff --git a/website/docs/guides/client-configurators.md b/website/docs/guides/client-configurators.md index 8cbdf101de..f981d45544 100644 --- a/website/docs/guides/client-configurators.md +++ b/website/docs/guides/client-configurators.md @@ -173,6 +173,24 @@ Some clients cannot be handled by the generic JSON configurator alone. - The bridge exposes a single proxy tool such as `unityMCP__call`, which then forwards to Unity MCP tool names. - OpenClaw support follows the currently selected MCP for Unity transport (via `openclaw-mcp-bridge`). +### DeepSeek Harness (native DSH bundle) + +- Uses a custom configurator (`DeepSeekHarnessConfigurator`) because DeepSeek Harness (`dsh`) consumes MCP servers through its `@deepseek-ai/dsh-mcp-client` plugin, registered as a Cordis row — not a JSON/TOML config file. +- Setup is manual, like the other UI-driven clients. The snippet renders a configuration-only bundle (`$DSH_HOME/bundles//unity-mcp/` with a `package.json` manifest and a `cordis.patch.yml` row) plus the `dsh plugin add` / `dsh plugin remove` commands. Run them with DSH closed (its `desktop` profile is Electron-managed) using the Desktop-installed `dsh`, then reopen it. After a DSH reload the tools surface as `mcp__unityMCP__`. +- The row shape (confirmed against DSH's `mcp-memory` guide and plugin-manager patch schema): + ```yaml + - insert: + - id: unitymcp + name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: unityMCP + transport: streamable-http # or stdio with command/args + url: http://127.0.0.1:/mcp + failOnStartupError: true + ``` +- Installing as a bundle (instead of editing `$DSH_HOME/cordis.patch.yml`, a global layer that collides with rows from other sources) keeps the row profile-scoped, and DSH's own plugin manager owns enable/disable and removal afterwards. +- Detection is fail-closed: DSH counts as installed only once it has run at least once (its home directory exists). + --- ## Adding a new MCP client (typical JSON case)