Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## [Unreleased]

### Added
- **Antigravity** is now a one-click client-config and Project Skills target. Configuration uses the workspace-local `.agents/mcp_config.json` with `mcpServers` / `serverUrl`, following the [current Antigravity MCP format](https://antigravity.google/docs/mcp/). MCP config, `.agents/skills/`, and the managed `AGENTS.md` block share the nearest Git root (or Unity project directory outside Git). Existing global entries are reported without being rewritten, and configuration never silently falls back to global scope.

### Fixed
- Endpoint matching and rename cleanup use the target's endpoint key, including Antigravity's `serverUrl`, so reconfiguring a renamed project retires its recorded previous entry without deleting other servers.
- Antigravity instruction paths and version checks agree with its workspace Skills location for nested Unity projects. Platform toggles preserve shared instructions and user-authored text; configuring another Unity project without Antigravity no longer deletes workspace Antigravity Skills. Another project's managed workspace guidance is protected from replacement.

### Pull requests and issues
- [PR #56](https://github.com/FunplayAI/funplay-unity-mcp/pull/56): Antigravity client configuration and Project Skills support, with workspace isolation and shared-instruction fixes.
- No GitHub issues were closed by these changes.

### Contributors
- Thanks [@dehuaichendragonplus](https://github.com/dehuaichendragonplus) for PR #56.

## [0.6.5] - 2026-09-03

### Added
Expand Down
147 changes: 126 additions & 21 deletions Editor/MCP/Server/FunplayMCPClientConfigPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void AddTo(VisualElement parent)

var skillsHint = new Label(skillsSupported
? "Configure + Skills also installs the project MCP workflow skill."
: "Project skills are currently available for Claude Code, Cursor, Codex, OpenCode, and DeepSeek Harness.");
: "Project skills are currently available for Claude Code, Cursor, Codex, OpenCode, DeepSeek Harness, and Antigravity.");
skillsHint.style.fontSize = 10;
skillsHint.style.color = new Color(0.6f, 0.6f, 0.6f);
skillsHint.style.marginBottom = 4;
Expand Down Expand Up @@ -196,6 +196,9 @@ public void RefreshStatus()
var resolvedKey = ResolveServerKeyForTarget(target);
var details = $"{target.ConfigPath}\nEntry: {resolvedKey} -> {GetServerUrl()}";

if (target.Name == "Antigravity")
details += GetAntigravityGlobalConfigNotice(GetUserHomePath());

// Say why the name grew a hash, or the user just sees an unexplained hex suffix. The
// occupant can also be this same project under a lost record (settings file deleted,
// fresh checkout on the same machine), which we cannot tell apart from another project.
Expand Down Expand Up @@ -473,6 +476,7 @@ private MCPConfigTarget[] CreateTargets(string homePath)
ConfigPath = FunplayDeepSeekHarnessPatch.GetDisplayPath(homePath),
IsDeepSeekHarness = true,
},
CreateAntigravityTarget(GetProjectRootPath()),
};
}

Expand Down Expand Up @@ -516,7 +520,7 @@ private void ConfigureMCPAndSkillsForTarget(MCPConfigTarget target)
EditorUtility.DisplayDialog(
"MCP Configuration",
configSummary + "\n\n" +
"Project skills are currently available for Claude Code, Cursor, Codex, OpenCode, and DeepSeek Harness.",
"Project skills are currently available for Claude Code, Cursor, Codex, OpenCode, DeepSeek Harness, and Antigravity.",
"OK");

_rebuildWindow?.Invoke();
Expand Down Expand Up @@ -826,9 +830,17 @@ private bool ConfigureProjectSkillsForPlatform(string platformId)
/// </summary>
private string ConfigureJsonTarget(MCPConfigTarget target, string presetServerKey = null)
{
var rootKey = GetRootKey(target);
var serverName = presetServerKey ?? ResolveServerKeyForTarget(target);
var entry = CreateHttpEntry(target);
WriteJsonConfiguration(target, serverName,
_settings.GetLastClientConfigKey(target.Name), CreateHttpEntry(target));
return serverName;
}

internal static void WriteJsonConfiguration(
MCPConfigTarget target, string serverName, string previousServerName,
Dictionary<string, object> entry)
{
var rootKey = GetRootKey(target);

Dictionary<string, object> root = null;
if (File.Exists(target.ConfigPath))
Expand All @@ -840,10 +852,13 @@ private string ConfigureJsonTarget(MCPConfigTarget target, string presetServerKe
: GetOrCreateNestedDictionary(root, rootKey);

servers[serverName] = entry;
RemoveSupersededFunplayEntries(servers, serverName, _settings.GetLastClientConfigKey(target.Name));
RemoveSupersededFunplayEntries(
servers, serverName, previousServerName, target.UrlFieldName);

var directory = Path.GetDirectoryName(target.ConfigPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
File.WriteAllText(target.ConfigPath, SimpleJsonHelper.Serialize(root));
return serverName;
}

/// <summary>
Expand All @@ -857,7 +872,7 @@ private string ConfigureJsonTarget(MCPConfigTarget target, string presetServerKe
/// nothing, the same "stop instead of destroy" stance <c>WriteManagedBlock</c> takes for a
/// legacy AGENTS.md. Returns null for an empty file (nothing to preserve).
/// </summary>
private Dictionary<string, object> ParseRewritableConfig(
private static Dictionary<string, object> ParseRewritableConfig(
MCPConfigTarget target, string serverName, Dictionary<string, object> entry)
{
var content = File.ReadAllText(target.ConfigPath);
Expand Down Expand Up @@ -1231,7 +1246,10 @@ internal static bool TryWriteTextAtomicallyIfUnchanged(
/// any project on the machine could have written it, so it is surfaced in the panel instead.
/// </summary>
internal static void RemoveSupersededFunplayEntries(
Dictionary<string, object> servers, string currentServerName, string previousServerName)
Dictionary<string, object> servers,
string currentServerName,
string previousServerName,
string urlFieldName = null)
{
if (string.IsNullOrEmpty(previousServerName) ||
string.Equals(previousServerName, currentServerName, StringComparison.Ordinal))
Expand All @@ -1248,17 +1266,19 @@ internal static void RemoveSupersededFunplayEntries(

// A recorded key whose entry now points somewhere non-local was edited by hand after we
// wrote it; leave that alone rather than deleting someone's deliberate change.
if (!IsLoopbackEntry(previousEntry))
if (!IsLoopbackEntry(previousEntry, urlFieldName))
return;

servers.Remove(previousServerName);
}

private static bool IsLoopbackEntry(object entry)
// The endpoint key follows the target (Antigravity writes serverUrl); reading only "url"
// here would make an Antigravity entry look hand-edited and never retire it.
private static bool IsLoopbackEntry(object entry, string urlFieldName = null)
{
var entryMap = entry as Dictionary<string, object>;
object url;
if (entryMap == null || !entryMap.TryGetValue("url", out url))
if (entryMap == null || !entryMap.TryGetValue(GetUrlFieldName(urlFieldName), out url))
return false;

return IsLoopbackUrl(url as string);
Expand Down Expand Up @@ -1425,7 +1445,8 @@ private static bool TargetEntryPointsAtUrl(
target.IsToml,
target.RootKey,
serverKey,
expectedUrl);
expectedUrl,
target.UrlFieldName);
}
catch (Exception)
{
Expand All @@ -1446,7 +1467,8 @@ internal static bool ConfigEntryPointsAtUrl(
bool isToml,
string rootKey,
string serverKey,
string expectedUrl)
string expectedUrl,
string urlFieldName = null)
{
if (string.IsNullOrEmpty(content) ||
string.IsNullOrEmpty(serverKey) ||
Expand Down Expand Up @@ -1482,7 +1504,7 @@ internal static bool ConfigEntryPointsAtUrl(
var entry = entryValue as Dictionary<string, object>;
object urlValue;
return entry != null &&
entry.TryGetValue("url", out urlValue) &&
entry.TryGetValue(GetUrlFieldName(urlFieldName), out urlValue) &&
string.Equals(urlValue as string, expectedUrl, StringComparison.OrdinalIgnoreCase);
}

Expand Down Expand Up @@ -1515,21 +1537,46 @@ private string BuildLMStudioConfiguredMessage()
}

private Dictionary<string, object> CreateHttpEntry(MCPConfigTarget target)
{
return CreateHttpEntry(
GetServerUrl(),
target.UrlFieldName,
target.IncludeTypeField,
target.TypeFieldValue,
target.IncludeEnabledField);
}

internal static Dictionary<string, object> CreateHttpEntry(
string serverUrl,
string urlFieldName,
bool includeTypeField,
string typeFieldValue,
bool includeEnabledField)
{
var entry = new Dictionary<string, object>
{
["url"] = GetServerUrl()
[GetUrlFieldName(urlFieldName)] = serverUrl
};

if (target.IncludeTypeField)
entry["type"] = string.IsNullOrEmpty(target.TypeFieldValue) ? "http" : target.TypeFieldValue;
if (includeTypeField)
entry["type"] = string.IsNullOrEmpty(typeFieldValue) ? "http" : typeFieldValue;

if (target.IncludeEnabledField)
if (includeEnabledField)
entry["enabled"] = true;

return entry;
}

/// <summary>
/// The JSON key the endpoint lives under: <c>url</c> unless the target names another one
/// (Antigravity's <c>serverUrl</c>). Every reader of an entry's endpoint must go through this
/// too, or an entry written under the alternate key is invisible to it.
/// </summary>
internal static string GetUrlFieldName(string urlFieldName)
{
return string.IsNullOrEmpty(urlFieldName) ? "url" : urlFieldName;
}

private string CreateTomlSection(MCPConfigTarget target, string serverKey)
{
if (!target.IsToml)
Expand Down Expand Up @@ -1749,6 +1796,56 @@ private static string GetOpenCodeConfigPath()
return Path.Combine(GetProjectScopeKeyPath(), ".opencode", "opencode.json");
}

/// <summary>
/// Keep Antigravity's MCP config, skills and AGENTS.md in one workspace. Use the nearest
/// repository root (including worktrees), or the Unity project itself outside Git.
/// See https://antigravity.google/docs/mcp/ for workspace config and serverUrl support.
/// </summary>
internal static string GetAntigravityWorkspaceRoot(string projectRoot)
{
if (string.IsNullOrWhiteSpace(projectRoot))
throw new ArgumentException("A Unity project root is required.", nameof(projectRoot));
return FindGitRootOrSelf(Path.GetFullPath(projectRoot));
}

internal static string GetAntigravityConfigPath(string projectRoot)
{
return Path.Combine(GetAntigravityWorkspaceRoot(projectRoot), ".agents", "mcp_config.json");
}

internal static MCPConfigTarget CreateAntigravityTarget(string projectRoot)
{
return new MCPConfigTarget
{
Name = "Antigravity",
ConfigPath = GetAntigravityConfigPath(projectRoot),
UrlFieldName = "serverUrl",
ActivationHint =
"Open this workspace in a current Antigravity version and reload its MCP servers:\n" +
GetAntigravityWorkspaceRoot(projectRoot) +
"\nNested Unity projects use their repository root as the workspace."
};
}

// Older previews wrote a global entry. Merely finding a matching name is not proof that
// it belongs to this project, so report existing entries without rewriting either file.
internal static string GetAntigravityGlobalConfigNotice(string homePath)
{
var notices = new List<string>();
foreach (var directory in new[] { "config", "antigravity" })
{
var path = Path.Combine(homePath, ".gemini", directory, "mcp_config.json");
var names = ReadFunplayEntryNames(new MCPConfigTarget { ConfigPath = path });
if (names.Count > 0)
notices.Add($"{path}: {string.Join(", ", names.OrderBy(name => name, StringComparer.Ordinal))}");
}

return notices.Count == 0 ? string.Empty :
"\nGlobal Funplay entries also exist and remain visible in other workspaces:\n" +
string.Join("\n", notices) +
"\nReview them after configuring each workspace; Configure leaves these global files unchanged.";
}

private static string GetUserHomePath()
{
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Expand Down Expand Up @@ -1851,7 +1948,7 @@ private static string GetVSCodeConfigPath(string homePath)
return Path.Combine(homePath, ".vscode", "mcp.json");
}

private struct MCPConfigTarget
internal struct MCPConfigTarget
{
public string Name;
public string ConfigPath;
Expand All @@ -1876,6 +1973,14 @@ private struct MCPConfigTarget
/// </summary>
public string TypeFieldValue;

/// <summary>
/// Key the endpoint is written under inside the entry. Empty means <c>url</c>, which is
/// what every client here uses except Antigravity: its <c>McpServerSpec</c> names the
/// remote endpoint <c>serverUrl</c> and ignores an entry carrying neither that nor
/// <c>command</c>.
/// </summary>
public string UrlFieldName;

/// <summary>
/// True for OpenCode, whose documented remote-server example spells the flag out
/// (<c>{"type":"remote","url":...,"enabled":true}</c>). The field is optional in its schema
Expand All @@ -1885,8 +1990,8 @@ private struct MCPConfigTarget

/// <summary>
/// True only for Claude Code. Its config file supports a <c>projects["&lt;path&gt;"]</c>
/// section that Claude Code applies only to sessions opened at that path; every other
/// client here has no such concept and keeps writing at the config's top level.
/// section that Claude Code applies only to sessions opened at that path. Other clients
/// write at the config's top level, which may itself be a workspace-local file.
/// </summary>
public bool UseProjectScope;
}
Expand Down
8 changes: 4 additions & 4 deletions Editor/MCP/Server/FunplayProjectSkillsWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private void BuildPlatformSection()
var section = CreateSection();
section.Add(CreateSectionHeader("Current Platform"));

_platformTargets = new[] { "Claude Code", "Cursor", "VS Code", "Trae", "Kiro", "Codex", "OpenCode", "DeepSeek Harness" };
_platformTargets = new[] { "Claude Code", "Cursor", "VS Code", "Trae", "Kiro", "Codex", "OpenCode", "DeepSeek Harness", "Antigravity" };
_selectedTargetIndex = Mathf.Clamp(_selectedTargetIndex, 0, _platformTargets.Length - 1);
var persistedTargetName = _settingsController.MCPSelectedConfigTarget;
if (!string.IsNullOrWhiteSpace(persistedTargetName))
Expand Down Expand Up @@ -136,7 +136,7 @@ private void BuildPlatformSection()

if (!currentPlatformSupported)
{
section.Add(CreateHint("Project skills integration is not available for this platform yet. Supported platforms: Codex, Claude Code, Cursor, OpenCode.", new Color(1f, 0.75f, 0.45f)));
section.Add(CreateHint("Project skills integration is not available for this platform yet. Supported platforms: Codex, Claude Code, Cursor, OpenCode, DeepSeek Harness, Antigravity.", new Color(1f, 0.75f, 0.45f)));
}

_mainContainer.Add(section);
Expand Down Expand Up @@ -331,7 +331,7 @@ private void ApplyProjectSkillsConfiguration()
{
EditorUtility.DisplayDialog(
"Project Skills Configuration",
"Project skills are not supported for the currently selected platform yet.\n\nPlease select Codex, Claude Code, Cursor, or OpenCode.",
"Project skills are not supported for the currently selected platform yet.\n\nPlease select Codex, Claude Code, Cursor, OpenCode, DeepSeek Harness, or Antigravity.",
"OK");
return;
}
Expand Down Expand Up @@ -388,7 +388,7 @@ private void UpgradeProjectSkills()
{
EditorUtility.DisplayDialog(
"Project Skills Upgrade",
"Project skills are not supported for the currently selected platform yet.\n\nPlease select Codex, Claude Code, Cursor, or OpenCode.",
"Project skills are not supported for the currently selected platform yet.\n\nPlease select Codex, Claude Code, Cursor, OpenCode, DeepSeek Harness, or Antigravity.",
"OK");
return;
}
Expand Down
Loading
Loading