Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ The main documentation for the site is organized into the following sections:
:caption: MCP

mcp/parallel-search
mcp/sem

.. _llamasharp:

Expand Down
19 changes: 19 additions & 0 deletions docs/mcp/sem.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
}
}
59 changes: 59 additions & 0 deletions docs/mcp/sem.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<None Include="..\..\docs\mcp\parallel-search.json" Link="parallel-search.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\docs\mcp\sem.json" Link="sem.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
Expand Down
57 changes: 57 additions & 0 deletions tests/BotSharp.Core.UnitTests/MCP/SemExampleTests.cs
Original file line number Diff line number Diff line change
@@ -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<McpSettings>();
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<McpClientManager>().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<string, object?>
{
["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));
}
}