Repository navigation
feat: add WorkBuddy MCP client configuration #1457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
haranishi
wants to merge
4
commits into
CoplayDev:beta
Choose a base branch
from
haranishi:feat/workbuddy-client-1402
base: beta
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+279
−9
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e7b88ce
feat: WorkBuddyのMCP設定に対応 #1402
haranishi 05e0b3a
docs: list WorkBuddy in the per-client toggle section
haranishi 6c9d1bc
fix: preserve existing client configuration on read and parse errors
haranishi 4d5c478
fix: isolate atomic client configuration backups
haranishi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
MCPForUnity/Editor/Clients/Configurators/WorkBuddyConfigurator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using MCPForUnity.Editor.Models; | ||
|
|
||
| namespace MCPForUnity.Editor.Clients.Configurators | ||
| { | ||
| public class WorkBuddyConfigurator : JsonFileMcpConfigurator | ||
| { | ||
| public WorkBuddyConfigurator() : base(new McpClient | ||
| { | ||
| name = "WorkBuddy", | ||
| windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".workbuddy", "mcp.json"), | ||
| macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".workbuddy", "mcp.json"), | ||
| linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".workbuddy", "mcp.json"), | ||
| }) | ||
| { } | ||
|
|
||
| public override IList<string> GetInstallationSteps() => new List<string> | ||
| { | ||
| "Install and launch WorkBuddy to create its '~/.workbuddy' directory", | ||
| "Click Configure to add UnityMCP to '~/.workbuddy/mcp.json'", | ||
| "In WorkBuddy, open Plugins > MCP Servers and enable UnityMCP", | ||
| "Restart WorkBuddy or reconnect the server if needed" | ||
| }; | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
MCPForUnity/Editor/Clients/Configurators/WorkBuddyConfigurator.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/WorkBuddyConfiguratorTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using MCPForUnity.Editor.Clients; | ||
| using MCPForUnity.Editor.Clients.Configurators; | ||
| using MCPForUnity.Editor.Constants; | ||
| using MCPForUnity.Editor.Helpers; | ||
| using MCPForUnity.Editor.Models; | ||
| using MCPForUnity.Editor.Services; | ||
| using Newtonsoft.Json.Linq; | ||
| using NUnit.Framework; | ||
| using UnityEditor; | ||
|
|
||
| namespace MCPForUnityTests.Editor.Clients | ||
| { | ||
| [TestFixture] | ||
| public class WorkBuddyConfiguratorTests : TransportPreferenceTestBase | ||
| { | ||
| private string _tempDir; | ||
| private readonly Dictionary<string, string> _savedPrefs = new(); | ||
| private bool _hadLock; | ||
| private bool _locked; | ||
|
|
||
| [SetUp] | ||
| public void SetUp() | ||
| { | ||
| foreach (string key in new[] { EditorPrefKeys.HttpTransportScope, EditorPrefKeys.HttpBaseUrl }) | ||
| _savedPrefs[key] = EditorPrefs.HasKey(key) ? EditorPrefs.GetString(key) : null; | ||
| _hadLock = EditorPrefs.HasKey(EditorPrefKeys.LockCursorConfig); | ||
| _locked = EditorPrefs.GetBool(EditorPrefKeys.LockCursorConfig, false); | ||
| EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, "local"); | ||
| EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080"); | ||
| EditorPrefs.SetBool(EditorPrefKeys.LockCursorConfig, false); | ||
| EditorConfigurationCache.Instance.Refresh(); | ||
| _tempDir = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N")); | ||
| Directory.CreateDirectory(_tempDir); | ||
| } | ||
|
|
||
| [TearDown] | ||
| public void TearDown() | ||
| { | ||
| foreach (var pref in _savedPrefs) | ||
| { | ||
| if (pref.Value == null) EditorPrefs.DeleteKey(pref.Key); | ||
| else EditorPrefs.SetString(pref.Key, pref.Value); | ||
| } | ||
| _savedPrefs.Clear(); | ||
| if (_hadLock) EditorPrefs.SetBool(EditorPrefKeys.LockCursorConfig, _locked); | ||
| else EditorPrefs.DeleteKey(EditorPrefKeys.LockCursorConfig); | ||
| EditorConfigurationCache.Instance.Refresh(); | ||
| if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Registry_DiscoversWorkBuddyAsASeparateClient() | ||
| { | ||
| var workBuddy = McpClientRegistry.All.Single(c => c.Id == "workbuddy"); | ||
| Assert.IsInstanceOf<WorkBuddyConfigurator>(workBuddy); | ||
| Assert.AreEqual("WorkBuddy", workBuddy.DisplayName); | ||
| Assert.AreNotEqual(new CodeBuddyCliConfigurator().GetConfigPath(), workBuddy.GetConfigPath()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ConfigPath_UsesWorkBuddyUserConfigOnEveryPlatform() | ||
| { | ||
| var configurator = new WorkBuddyConfigurator(); | ||
| string expected = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".workbuddy", "mcp.json"); | ||
| Assert.AreEqual(expected, configurator.Client.windowsConfigPath); | ||
| Assert.AreEqual(expected, configurator.Client.macConfigPath); | ||
| Assert.AreEqual(expected, configurator.Client.linuxConfigPath); | ||
| } | ||
|
|
||
| [TestCase(true)] | ||
| [TestCase(false)] | ||
| public void Configure_RoundTripsTransportAndPreservesOtherSettings(bool http) | ||
| { | ||
| EditorConfigurationCache.Instance.SetUseHttpTransport(http); | ||
| var configurator = new TempWorkBuddyConfigurator(Path.Combine(_tempDir, "mcp.json")); | ||
| File.WriteAllText(configurator.GetConfigPath(), "{\"theme\":\"dark\",\"mcpServers\":{\"other\":{\"command\":\"other-server\"}}}"); | ||
|
|
||
| configurator.Configure(); | ||
| string firstWrite = File.ReadAllText(configurator.GetConfigPath()); | ||
| configurator.Configure(); | ||
| Assert.AreEqual(firstWrite, File.ReadAllText(configurator.GetConfigPath()), "Repeated configuration must be idempotent"); | ||
| Assert.AreEqual(McpStatus.Configured, configurator.CheckStatus(attemptAutoRewrite: false)); | ||
| Assert.AreEqual(http ? ConfiguredTransport.Http : ConfiguredTransport.Stdio, configurator.ConfiguredTransport); | ||
|
|
||
| var root = JObject.Parse(firstWrite); | ||
| Assert.AreEqual("dark", (string)root["theme"]); | ||
| Assert.AreEqual("other-server", (string)root["mcpServers"]?["other"]?["command"]); | ||
| var unity = (JObject)root["mcpServers"]?["unityMCP"]; | ||
| Assert.IsNotNull(unity); | ||
| Assert.AreEqual(http ? "http" : "stdio", (string)unity["type"]); | ||
| if (http) | ||
| { | ||
| Assert.AreEqual("http://127.0.0.1:8080/mcp", (string)unity["url"]); | ||
| Assert.IsNull(unity["command"]); | ||
| } | ||
| else | ||
| { | ||
| Assert.IsNotEmpty((string)unity["command"]); | ||
| CollectionAssert.Contains(unity["args"].ToObject<string[]>(), "stdio"); | ||
| Assert.IsNull(unity["url"]); | ||
| } | ||
|
|
||
| configurator.Unregister(); | ||
| root = JObject.Parse(File.ReadAllText(configurator.GetConfigPath())); | ||
| Assert.IsNull(root["mcpServers"]?["unityMCP"]); | ||
| Assert.AreEqual("other-server", (string)root["mcpServers"]?["other"]?["command"]); | ||
| Assert.AreEqual("dark", (string)root["theme"]); | ||
| Assert.AreEqual(McpStatus.NotConfigured, configurator.Status); | ||
| } | ||
|
|
||
| [TestCase(true)] | ||
| [TestCase(false)] | ||
| public void Configure_InvalidJsonLeavesExistingFileUnchanged(bool http) | ||
| { | ||
| EditorConfigurationCache.Instance.SetUseHttpTransport(http); | ||
| var configurator = new TempWorkBuddyConfigurator(Path.Combine(_tempDir, "mcp.json")); | ||
| const string incompleteJson = "{\"theme\":\"dark\",\"mcpServers\":{\"other\":{\"command\":\"other-server\"}}"; | ||
| File.WriteAllText(configurator.GetConfigPath(), incompleteJson); | ||
| byte[] original = File.ReadAllBytes(configurator.GetConfigPath()); | ||
|
|
||
| var error = Assert.Throws<InvalidOperationException>(() => configurator.Configure()); | ||
|
|
||
| StringAssert.Contains("Fix its JSON", error.Message); | ||
| CollectionAssert.AreEqual(original, File.ReadAllBytes(configurator.GetConfigPath())); | ||
| Assert.IsFalse(File.Exists(configurator.GetConfigPath() + ".tmp")); | ||
| Assert.IsFalse(File.Exists(configurator.GetConfigPath() + ".backup")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Configure_UnreadableExistingFileIsLeftUnchanged() | ||
| { | ||
| var configurator = new TempWorkBuddyConfigurator(Path.Combine(_tempDir, "mcp.json")); | ||
| const string existingJson = "{\"theme\":\"dark\",\"mcpServers\":{\"other\":{\"command\":\"other-server\"}}}"; | ||
| File.WriteAllText(configurator.GetConfigPath(), existingJson); | ||
| byte[] original = File.ReadAllBytes(configurator.GetConfigPath()); | ||
|
|
||
| using (new FileStream(configurator.GetConfigPath(), FileMode.Open, FileAccess.ReadWrite, FileShare.None)) | ||
| { | ||
| var error = Assert.Throws<InvalidOperationException>(() => configurator.Configure()); | ||
| StringAssert.Contains("Could not read", error.Message); | ||
| } | ||
|
|
||
| CollectionAssert.AreEqual(original, File.ReadAllBytes(configurator.GetConfigPath())); | ||
| } | ||
|
|
||
| [TestCase(true)] | ||
| [TestCase(false)] | ||
| public void Configure_PreservesExistingTemporaryAndBackupArtifacts(bool temporaryIsDirectory) | ||
| { | ||
| var configurator = new TempWorkBuddyConfigurator(Path.Combine(_tempDir, "mcp.json")); | ||
| string path = configurator.GetConfigPath(); | ||
| File.WriteAllText(path, "{\"theme\":\"current\",\"mcpServers\":{\"other\":{\"command\":\"other-server\"}}}"); | ||
| const string temporaryContents = "user temporary contents"; | ||
| if (temporaryIsDirectory) Directory.CreateDirectory(path + ".tmp"); | ||
| else File.WriteAllText(path + ".tmp", temporaryContents); | ||
| File.WriteAllText(path + ".backup", "{\"theme\":\"stale backup\"}"); | ||
| byte[] originalBackup = File.ReadAllBytes(path + ".backup"); | ||
| byte[] originalTemporary = temporaryIsDirectory ? null : File.ReadAllBytes(path + ".tmp"); | ||
| string[] originalEntries = Directory.GetFileSystemEntries(_tempDir); | ||
|
|
||
| configurator.Configure(); | ||
|
|
||
| var root = JObject.Parse(File.ReadAllText(path)); | ||
| Assert.AreEqual("current", (string)root["theme"]); | ||
| Assert.AreEqual("other-server", (string)root["mcpServers"]["other"]["command"]); | ||
| Assert.IsNotNull(root["mcpServers"]["unityMCP"]); | ||
| CollectionAssert.AreEqual(originalBackup, File.ReadAllBytes(path + ".backup")); | ||
| if (temporaryIsDirectory) Assert.IsTrue(Directory.Exists(path + ".tmp")); | ||
| else CollectionAssert.AreEqual(originalTemporary, File.ReadAllBytes(path + ".tmp")); | ||
| CollectionAssert.AreEquivalent(originalEntries, Directory.GetFileSystemEntries(_tempDir)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void AtomicWrite_FailurePreservesExistingBackupAndCleansOwnTemporaryFiles() | ||
| { | ||
| string path = Path.Combine(_tempDir, "destination-directory"); | ||
| Directory.CreateDirectory(path); | ||
| string child = Path.Combine(path, "keep.txt"); | ||
| File.WriteAllText(child, "keep directory contents"); | ||
| File.WriteAllText(path + ".backup", "user backup contents"); | ||
| byte[] originalBackup = File.ReadAllBytes(path + ".backup"); | ||
| string[] originalEntries = Directory.GetFileSystemEntries(_tempDir); | ||
|
|
||
| Assert.Throws<Exception>(() => McpConfigurationHelper.WriteAtomicFile(path, "replacement")); | ||
|
|
||
| Assert.AreEqual("keep directory contents", File.ReadAllText(child)); | ||
| CollectionAssert.AreEqual(originalBackup, File.ReadAllBytes(path + ".backup")); | ||
| CollectionAssert.AreEquivalent(originalEntries, Directory.GetFileSystemEntries(_tempDir)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void IsInstalled_RequiresTheWorkBuddyDirectory() | ||
| { | ||
| var configurator = new TempWorkBuddyConfigurator(Path.Combine(_tempDir, ".workbuddy", "mcp.json")); | ||
| Assert.IsFalse(configurator.IsInstalled); | ||
| Directory.CreateDirectory(Path.GetDirectoryName(configurator.GetConfigPath())); | ||
| Assert.IsTrue(configurator.IsInstalled); | ||
| Assert.IsFalse(File.Exists(configurator.GetConfigPath())); | ||
| } | ||
|
|
||
| private sealed class TempWorkBuddyConfigurator : WorkBuddyConfigurator | ||
| { | ||
| private readonly string _path; | ||
| public TempWorkBuddyConfigurator(string path) => _path = path; | ||
| public override string GetConfigPath() => _path; | ||
| } | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/WorkBuddyConfiguratorTests.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.