diff --git a/docs/index.rst b/docs/index.rst index c82e3b2d4..e1dfd40ce 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -101,6 +101,7 @@ The main documentation for the site is organized into the following sections: :caption: MCP mcp/parallel-search + mcp/sem .. _llamasharp: diff --git a/docs/mcp/sem.json b/docs/mcp/sem.json new file mode 100644 index 000000000..96d875c05 --- /dev/null +++ b/docs/mcp/sem.json @@ -0,0 +1,19 @@ +{ + "MCP": { + "Enabled": true, + "McpServerConfigs": [ + { + "Id": "sem", + "Name": "Sem local code", + "Enabled": true, + "StdioConfig": { + "Command": "sem", + "Arguments": ["mcp"], + "EnvironmentVariables": { + "SEM_REPO": "/absolute/path/to/checkout" + } + } + } + ] + } +} diff --git a/docs/mcp/sem.md b/docs/mcp/sem.md new file mode 100644 index 000000000..6ba7c2336 --- /dev/null +++ b/docs/mcp/sem.md @@ -0,0 +1,59 @@ +# Local code lookup with Sem + +BotSharp's stdio MCP integration can start [Sem](https://github.com/Ataraxy-Labs/sem) +to look up source code in a local checkout. This optional configuration does not +change WebStarter defaults or require another BotSharp plugin. + +## Configure the server + +Install Sem on the machine or container running BotSharp (`sem --version`; +tested with 0.27.0). Add the server object from [sem.json](sem.json) to your +existing `MCP.McpServerConfigs` array, preserving other servers and settings. +Replace `SEM_REPO` with an absolute checkout path visible to that process. +If `sem` is not on the host's `PATH`, use its absolute executable path in +`StdioConfig.Command`. Keep `MCP.Enabled` and the server's `Enabled` set to `true`. + +WebStarter already registers `AddBotSharpMCP(configuration)`. Hosts that register +services themselves need the same call. + +## Select code lookup tools + +In the MCP server list, select **Sem local code** and enable `sem_find` and +`sem_grep` for the agent. The equivalent agent `mcp_tools` entry is: + +```json +{ + "name": "Sem local code", + "server_id": "sem", + "disabled": false, + "functions": [{ "name": "sem_find" }, { "name": "sem_grep" }] +} +``` + +Ask the agent to find a known symbol in the selected checkout. For example, +`sem_find` accepts `{"query":"parse_config","mode":"context","token_budget":1200}`. +BotSharp routes selected functions through its existing MCP executor. The agent +still uses its configured LLM; Sem does not supply a model or model credentials. + +Sem may write a local index. `SEM_REPO` is a repository selector, not an access +control boundary. Run only against trusted checkouts under an appropriately +restricted OS account. The full server also exposes validation tools that can +execute commands, so retain the explicit tool selection. Source returned by +tools will be sent to the agent's configured model. + +Disable the server with `Enabled: false`, or remove this server entry and the +agent's corresponding `mcp_tools` entry. No existing provider settings need to +change. + +## Validate the example + +From the repository root with the .NET 8 SDK: + +```sh +dotnet test tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj --filter FullyQualifiedName~SemExampleTests +``` + +The configuration test does not launch Sem. To also exercise discovery and +`sem_find` through BotSharp's client manager, set `BOTSHARP_SEM_TEST_REPO` to an +absolute disposable checkout containing a `parse_config` symbol before running +the same command. The live test is opt-in; it requires Sem on `PATH` but no LLM. diff --git a/tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj b/tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj index 252d113e1..558d33cf8 100644 --- a/tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj +++ b/tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/BotSharp.Core.UnitTests/MCP/SemExampleTests.cs b/tests/BotSharp.Core.UnitTests/MCP/SemExampleTests.cs new file mode 100644 index 000000000..73b2cce97 --- /dev/null +++ b/tests/BotSharp.Core.UnitTests/MCP/SemExampleTests.cs @@ -0,0 +1,57 @@ +using BotSharp.Core.MCP; +using BotSharp.Core.MCP.Managers; +using BotSharp.Core.MCP.Settings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace BotSharp.Core.UnitTests.MCP; + +public class SemExampleTests +{ + private static IConfigurationRoot Configuration() => new ConfigurationBuilder() + .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "sem.json")) + .Build(); + + [Fact] + public void Configuration_selects_stdio_and_an_explicit_checkout() + { + var services = new ServiceCollection().AddLogging(); + services.AddBotSharpMCP(Configuration()); + using var provider = services.BuildServiceProvider(); + var settings = provider.GetRequiredService(); + Assert.True(settings.Enabled); + var server = Assert.Single(settings.McpServerConfigs); + Assert.Equal("sem", server.Id); + Assert.Null(server.HttpConfig); + Assert.Equal("sem", server.StdioConfig!.Command); + Assert.Equal(new[] { "mcp" }, server.StdioConfig.Arguments); + Assert.Equal("/absolute/path/to/checkout", server.StdioConfig.EnvironmentVariables!["SEM_REPO"]); + } + + [Fact] + public async Task Optional_live_lookup_uses_the_configured_stdio_client() + { + // Opt in with a disposable checkout containing a parse_config symbol. + // Normal CI does not need Sem installed or access to a user's checkout. + var repo = Environment.GetEnvironmentVariable("BOTSHARP_SEM_TEST_REPO"); + if (string.IsNullOrEmpty(repo)) return; + Assert.True(Path.IsPathRooted(repo) && Directory.Exists(repo)); + var configuration = Configuration(); + configuration["MCP:McpServerConfigs:0:StdioConfig:EnvironmentVariables:SEM_REPO"] = repo; + var services = new ServiceCollection().AddLogging(); + services.AddBotSharpMCP(configuration); + await using var provider = services.BuildServiceProvider(); + await using var client = await provider.GetRequiredService().GetMcpClientAsync("sem"); + Assert.NotNull(client); + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + var tools = await client.ListToolsAsync(cancellationToken: timeout.Token); + Assert.Contains(tools, tool => tool.Name == "sem_find"); + var result = await client.CallToolAsync("sem_find", new Dictionary + { + ["query"] = "parse_config", ["mode"] = "context", ["token_budget"] = 1200 + }, cancellationToken: timeout.Token); + Assert.NotEqual(true, result.IsError); + Assert.Contains("parse_config", System.Text.Json.JsonSerializer.Serialize(result)); + } +}