Share Dings MCP across agent sessions
This commit is contained in:
parent
e02faf6c92
commit
9051f4b6db
7 changed files with 187 additions and 10 deletions
37
tools/AquaSynthDingsMcp/AgentInstrumentRegistry.cs
Normal file
37
tools/AquaSynthDingsMcp/AgentInstrumentRegistry.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AquaSynth.Dings;
|
||||
|
||||
namespace AquaSynthDingsMcp;
|
||||
|
||||
public sealed class AgentInstrumentRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, string> assignments = new(StringComparer.Ordinal);
|
||||
private readonly string[] instrumentIds = DingCatalog.Instruments.Keys.Order(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
public string GetOrAssign(string agentSessionId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(agentSessionId);
|
||||
return assignments.GetOrAdd(agentSessionId, AssignDeterministically);
|
||||
}
|
||||
|
||||
public string Claim(string agentSessionId, string preferredInstrumentId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(agentSessionId);
|
||||
if (!DingCatalog.Instruments.TryGetValue(preferredInstrumentId, out var instrument))
|
||||
{
|
||||
throw new ArgumentException($"Unknown instrument '{preferredInstrumentId}'.");
|
||||
}
|
||||
|
||||
assignments[agentSessionId] = instrument.Id;
|
||||
return instrument.Id;
|
||||
}
|
||||
|
||||
private string AssignDeterministically(string agentSessionId)
|
||||
{
|
||||
var digest = SHA256.HashData(Encoding.UTF8.GetBytes(agentSessionId));
|
||||
var slot = BitConverter.ToUInt32(digest, 0) % (uint)instrumentIds.Length;
|
||||
return instrumentIds[slot];
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<ProjectReference Include="..\..\src\AquaSynth.Dings\AquaSynth.Dings.csproj" />
|
||||
<ProjectReference Include="..\AquaSynthDingsHost\AquaSynthDingsHost.csproj" ReferenceOutputAssembly="false" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -5,14 +5,24 @@ using AquaSynth.Dings;
|
|||
namespace AquaSynthDingsMcp;
|
||||
|
||||
[McpServerToolType]
|
||||
public sealed class DingTools(DingService service)
|
||||
public sealed class DingTools(DingService service, AgentInstrumentRegistry instruments)
|
||||
{
|
||||
[McpServerTool, Description("Synthesizes and audibly plays a semantic notification motif. Event says what happened; instrument identifies the agent by timbre.")]
|
||||
[McpServerTool, Description("Synthesizes and audibly plays a semantic notification motif using the stable instrument owned by this agent session.")]
|
||||
public Task<object> PlayDing(
|
||||
McpServer server,
|
||||
[Description("Semantic event such as task.complete or input.required.")] string eventId,
|
||||
[Description("Registered timbre such as warm-bell or glass-chime.")] string instrumentId,
|
||||
[Description("Playback gain from 0 to 1.")] float gain = .7f,
|
||||
CancellationToken cancellationToken = default) => service.PlayAsync(eventId, instrumentId, gain, cancellationToken);
|
||||
CancellationToken cancellationToken = default) =>
|
||||
service.PlayAsync(eventId, instruments.GetOrAssign(AgentSessionId(server)), gain, cancellationToken);
|
||||
|
||||
[McpServerTool, Description("Returns the stable instrument assigned to this agent session, assigning one automatically if needed.")]
|
||||
public object GetAgentInstrument(McpServer server) => Describe(instruments.GetOrAssign(AgentSessionId(server)));
|
||||
|
||||
[McpServerTool, Description("Lets this agent session claim a preferred registered instrument. Future play_ding calls use it automatically.")]
|
||||
public object ClaimInstrument(
|
||||
McpServer server,
|
||||
[Description("Registered timbre such as warm-bell or glass-chime.")] string preferredInstrumentId) =>
|
||||
Describe(instruments.Claim(AgentSessionId(server), preferredInstrumentId));
|
||||
|
||||
[McpServerTool, Description("Lists the semantic notification events and their meanings.")]
|
||||
public object ListDingEvents() => DingCatalog.Events.Values.Select(x => new { x.Id, x.Meaning, noteCount = x.Notes.Count }).ToArray();
|
||||
|
|
@ -31,4 +41,12 @@ public sealed class DingTools(DingService service)
|
|||
|
||||
[McpServerTool, Description("Mutes or unmutes the global AquaSynth Dings Windows audio session.")]
|
||||
public Task<DingsResponse> MuteDings(bool muted = true, CancellationToken cancellationToken = default) => service.SetMutedAsync(muted, cancellationToken);
|
||||
|
||||
private static string AgentSessionId(McpServer server) => server.SessionId ?? $"stdio:{Environment.ProcessId}";
|
||||
|
||||
private static object Describe(string instrumentId)
|
||||
{
|
||||
var instrument = DingCatalog.Instruments[instrumentId];
|
||||
return new { instrument.Id, instrument.Description, instrument.TimbreFamily };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using AquaSynthDingsMcp;
|
||||
using AquaSynth.Dings;
|
||||
|
||||
|
|
@ -29,10 +31,33 @@ if (args is ["--mute", var muteText] && bool.TryParse(muteText, out var muted))
|
|||
return;
|
||||
}
|
||||
|
||||
if (args is ["--http"] or ["--http", _])
|
||||
{
|
||||
var listenUrl = args.Length == 2 ? args[1] : "http://127.0.0.1:17878";
|
||||
using var singleton = new Mutex(initiallyOwned: true, "Local\\GameCult.AquaSynthDings.McpHttp.v1", out var ownsMutex);
|
||||
if (!ownsMutex) return;
|
||||
|
||||
var webBuilder = WebApplication.CreateBuilder([]);
|
||||
webBuilder.Logging.ClearProviders();
|
||||
webBuilder.WebHost.UseUrls(listenUrl);
|
||||
webBuilder.Services.AddSingleton<IDingsHostClient, DingsHostClient>();
|
||||
webBuilder.Services.AddSingleton<DingService>();
|
||||
webBuilder.Services.AddSingleton<AgentInstrumentRegistry>();
|
||||
webBuilder.Services.AddMcpServer()
|
||||
.WithHttpTransport(options => options.Stateless = false)
|
||||
.WithTools<DingTools>();
|
||||
|
||||
var app = webBuilder.Build();
|
||||
app.MapMcp("/mcp");
|
||||
await app.RunAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Services.AddSingleton<IDingsHostClient, DingsHostClient>();
|
||||
builder.Services.AddSingleton<DingService>();
|
||||
builder.Services.AddSingleton<AgentInstrumentRegistry>();
|
||||
builder.Services.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<DingTools>();
|
||||
|
|
|
|||
|
|
@ -8,12 +8,27 @@ All MCP instances are command bridges. One persistent
|
|||
Aqua synthesis child, mixer, master volume, and mute state. Windows Volume
|
||||
Mixer and MCP volume tools address the same session.
|
||||
|
||||
Do not register this STDIO bridge globally in an agent harness that creates a
|
||||
Do not register the STDIO bridge globally in an agent harness that creates a
|
||||
separate MCP client for every root agent, subagent, or critic. STDIO ownership
|
||||
means one resident .NET bridge per client even though audio authority is already
|
||||
shared by `AquaSynthDingsHost.exe`. Keep the bridge disabled in that topology,
|
||||
use the direct CLI for local smokes, or expose one supervised Streamable HTTP MCP
|
||||
server when notifications need to be available across many concurrent agents.
|
||||
shared by `AquaSynthDingsHost.exe`. Run one supervised Streamable HTTP MCP server
|
||||
instead and point every agent at its loopback endpoint:
|
||||
|
||||
```powershell
|
||||
AquaSynthDingsMcp.exe --http http://127.0.0.1:17878
|
||||
```
|
||||
|
||||
The HTTP server is machine-singleton and keeps only MCP session-to-instrument
|
||||
assignments. Each agent session is assigned a stable timbre automatically and
|
||||
may claim another with `claim_instrument`; `play_ding` then uses that assignment
|
||||
without an instrument argument. `AquaSynthDingsHost.exe` remains the sole
|
||||
audio-session and playback owner.
|
||||
|
||||
Install the per-user singleton on Windows:
|
||||
|
||||
```powershell
|
||||
.\scripts\install-dings-mcp-task.ps1
|
||||
```
|
||||
|
||||
```powershell
|
||||
dotnet run --project tools/AquaSynthDingsMcp
|
||||
|
|
@ -28,8 +43,9 @@ dotnet run --project tools/AquaSynthDingsMcp -- --set-volume 0.5
|
|||
dotnet run --project tools/AquaSynthDingsMcp -- --mute true
|
||||
```
|
||||
|
||||
MCP tools are `play_ding`, `list_ding_events`, `list_instruments`,
|
||||
`stop_dings`, `get_volume`, `set_volume`, and `mute_dings`.
|
||||
MCP tools are `play_ding`, `get_agent_instrument`, `claim_instrument`,
|
||||
`list_ding_events`, `list_instruments`, `stop_dings`, `get_volume`,
|
||||
`set_volume`, and `mute_dings`.
|
||||
|
||||
Instrument availability is catalog-only. Additions must satisfy mechanical
|
||||
curation checks and then survive listening/identification tests; see
|
||||
|
|
|
|||
Loading…
Reference in a new issue