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));
+ }
+}