Skip to content

Make AIShell an MCP client to expose MCP tools to its agents #392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 24, 2025
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
1 change: 1 addition & 0 deletions shell/AIShell.Abstraction/AIShell.Abstraction.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
</ItemGroup>
</Project>
22 changes: 22 additions & 0 deletions shell/AIShell.Abstraction/IShell.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.Extensions.AI;

namespace AIShell.Abstraction;

/// <summary>
Expand Down Expand Up @@ -27,6 +29,26 @@ public interface IShell
/// <returns>A list of code blocks or null if there is no code block.</returns>
List<CodeBlock> ExtractCodeBlocks(string text, out List<SourceInfo> sourceInfos);

/// <summary>
/// Get available <see cref="AIFunction"/> instances for LLM to use.
/// </summary>
/// <returns></returns>
Task<List<AIFunction>> GetAIFunctions();

/// <summary>
/// Call an AI function.
/// </summary>
/// <param name="functionCall">A <see cref="FunctionCallContent"/> instance representing the function call request.</param>
/// <param name="captureException">Whether or not to capture the exception thrown from calling the tool.</param>
/// <param name="includeDetailedErrors">Whether or not to include the exception message to the message of the call result.</param>
/// <param name="cancellationToken">The cancellation token to cancel the call.</param>
/// <returns></returns>
Task<FunctionResultContent> CallAIFunction(
FunctionCallContent functionCall,
bool captureException,
bool includeDetailedErrors,
CancellationToken cancellationToken);

// TODO:
// - methods to run code: python, command-line, powershell, node-js.
// - methods to communicate with shell client.
Expand Down
1 change: 0 additions & 1 deletion shell/AIShell.App/AIShell.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

<ItemGroup>
<ProjectReference Include="..\AIShell.Kernel\AIShell.Kernel.csproj" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.7" />
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions shell/AIShell.Kernel/AIShell.Kernel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.47.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Spectre.Console.Json" Version="0.50.0" />
<PackageReference Include="ModelContextProtocol.Core" Version="0.2.0-preview.3" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions shell/AIShell.Kernel/Command/CommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal CommandRunner(Shell shell)
new RefreshCommand(),
new RetryCommand(),
new HelpCommand(),
new McpCommand(),
//new RenderCommand(),
};

Expand Down
40 changes: 40 additions & 0 deletions shell/AIShell.Kernel/Command/McpCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.CommandLine;
using AIShell.Abstraction;

namespace AIShell.Kernel.Commands;

internal sealed class McpCommand : CommandBase
{
public McpCommand()
: base("mcp", "Command for managing MCP servers and tools.")
{
this.SetHandler(ShowMCPData);

//var start = new Command("start", "Start an MCP server.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these commends be removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code was left intentionally. The code was for adding /mcp start and /mcp stop, which are follow-up work listed in the PR description.

//var stop = new Command("stop", "Stop an MCP server.");
//var server = new Argument<string>(
// name: "server",
// getDefaultValue: () => null,
// description: "Name of an MCP server.").AddCompletions(AgentCompleter);

//start.AddArgument(server);
//start.SetHandler(StartMcpServer, server);

//stop.AddArgument(server);
//stop.SetHandler(StopMcpServer, server);
}

private void ShowMCPData()
{
var shell = (Shell)Shell;
var host = shell.Host;

if (shell.McpManager.McpServers.Count is 0)
{
host.WriteErrorLine("No MCP server is available.");
return;
}

host.RenderMcpServersAndTools(shell.McpManager);
}
}
151 changes: 143 additions & 8 deletions shell/AIShell.Kernel/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
using System.Text;

using AIShell.Abstraction;
using AIShell.Kernel.Mcp;
using Markdig.Helpers;
using Microsoft.PowerShell;
using Spectre.Console;
using Spectre.Console.Json;
using Spectre.Console.Rendering;

namespace AIShell.Kernel;

Expand Down Expand Up @@ -175,7 +178,7 @@ public void RenderFullResponse(string response)
/// <inheritdoc/>
public void RenderTable<T>(IList<T> sources)
{
RequireStdoutOrStderr(operation: "render table");
RequireStdout(operation: "render table");
ArgumentNullException.ThrowIfNull(sources);

if (sources.Count is 0)
Expand All @@ -198,7 +201,7 @@ public void RenderTable<T>(IList<T> sources)
/// <inheritdoc/>
public void RenderTable<T>(IList<T> sources, IList<IRenderElement<T>> elements)
{
RequireStdoutOrStderr(operation: "render table");
RequireStdout(operation: "render table");

ArgumentNullException.ThrowIfNull(sources);
ArgumentNullException.ThrowIfNull(elements);
Expand Down Expand Up @@ -240,7 +243,7 @@ public void RenderTable<T>(IList<T> sources, IList<IRenderElement<T>> elements)
/// <inheritdoc/>
public void RenderList<T>(T source)
{
RequireStdoutOrStderr(operation: "render list");
RequireStdout(operation: "render list");
ArgumentNullException.ThrowIfNull(source);

if (source is IDictionary<string, string> dict)
Expand Down Expand Up @@ -271,7 +274,7 @@ public void RenderList<T>(T source)
/// <inheritdoc/>
public void RenderList<T>(T source, IList<IRenderElement<T>> elements)
{
RequireStdoutOrStderr(operation: "render list");
RequireStdout(operation: "render list");

ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(elements);
Expand Down Expand Up @@ -313,7 +316,7 @@ public void RenderList<T>(T source, IList<IRenderElement<T>> elements)
public void RenderDivider(string text, DividerAlignment alignment)
{
ArgumentException.ThrowIfNullOrEmpty(text);
RequireStdoutOrStderr(operation: "render divider");
RequireStdout(operation: "render divider");

if (!text.Contains("[/]"))
{
Expand Down Expand Up @@ -550,15 +553,134 @@ public string PromptForArgument(ArgumentInfo argInfo, bool printCaption)
internal void RenderReferenceText(string header, string content)
{
RequireStdoutOrStderr(operation: "Render reference");
IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console;

var panel = new Panel($"\n[italic]{content.EscapeMarkup()}[/]\n")
.RoundedBorder()
.BorderColor(Color.DarkCyan)
.Header($"[orange3 on italic] {header.Trim()} [/]");

AnsiConsole.WriteLine();
AnsiConsole.Write(panel);
AnsiConsole.WriteLine();
ansiConsole.WriteLine();
ansiConsole.Write(panel);
ansiConsole.WriteLine();
}

/// <summary>
/// Render the MCP tool call request.
/// </summary>
/// <param name="tool">The MCP tool.</param>
/// <param name="jsonArgs">The arguments in JSON form to be sent for the tool call.</param>
internal void RenderToolCallRequest(McpTool tool, string jsonArgs)
{
RequireStdoutOrStderr(operation: "render tool call request");
IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console;

bool hasArgs = !string.IsNullOrEmpty(jsonArgs);
IRenderable content = new Markup($"""

[bold]Run [olive]{tool.OriginalName}[/] from [olive]{tool.ServerName}[/] (MCP server)[/]

{tool.Description}

Input:{(hasArgs ? string.Empty : " <none>")}
""");

if (hasArgs)
{
var json = new JsonText(jsonArgs)
.MemberColor(Color.Aqua)
.ColonColor(Color.White)
.CommaColor(Color.White)
.StringStyle(Color.Tan);

content = new Grid()
.AddColumn(new GridColumn())
.AddRow(content)
.AddRow(json);
}

var panel = new Panel(content)
.Expand()
.RoundedBorder()
.Header("[green] Tool Call Request [/]")
.BorderColor(Color.Grey);

ansiConsole.WriteLine();
ansiConsole.Write(panel);
FancyStreamRender.ConsoleUpdated();
}

/// <summary>
/// Render a table with information about available MCP servers and tools.
/// </summary>
/// <param name="mcpManager">The MCP manager instance.</param>
internal void RenderMcpServersAndTools(McpManager mcpManager)
{
RequireStdout(operation: "render MCP servers and tools");

var toolTable = new Table()
.LeftAligned()
.SimpleBorder()
.BorderColor(Color.Green);

toolTable.AddColumn("[green bold]Server[/]");
toolTable.AddColumn("[green bold]Tool[/]");
toolTable.AddColumn("[green bold]Description[/]");

List<(string name, string status, string info)> readyServers = null, startingServers = null, failedServers = null;
foreach (var (name, server) in mcpManager.McpServers)
{
(int code, string status, string info) = server.IsInitFinished
? server.Error is null
? (1, "[green]\u2713 Ready[/]", string.Empty)
: (-1, "[red]\u2717 Failed[/]", $"[red]{server.Error.Message.EscapeMarkup()}[/]")
: (0, "[yellow]\u25CB Starting[/]", string.Empty);

var list = code switch
{
1 => readyServers ??= [],
0 => startingServers ??= [],
_ => failedServers ??= [],
};

list.Add((name, status, info));
}

if (startingServers is not null)
{
foreach (var (name, status, info) in startingServers)
{
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
}
}

if (failedServers is not null)
{
foreach (var (name, status, info) in failedServers)
{
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
}
}

if (readyServers is not null)
{
foreach (var (name, status, info) in readyServers)
{
if (toolTable.Rows is { Count: > 0 })
{
toolTable.AddEmptyRow();
}

var server = mcpManager.McpServers[name];
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
foreach (var item in server.Tools)
{
toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), item.Value.Description.EscapeMarkup());
}
}
}

AnsiConsole.Write(toolTable);
}

private static Spinner GetSpinner(SpinnerKind? kind)
Expand All @@ -583,6 +705,19 @@ private void RequireStdin(string operation)
}
}

/// <summary>
/// Throw exception if standard output is redirected.
/// </summary>
/// <param name="operation">The intended operation.</param>
/// <exception cref="InvalidOperationException">Throw the exception if stdout is redirected.</exception>
private void RequireStdout(string operation)
{
if (_outputRedirected)
{
throw new InvalidOperationException($"Cannot {operation} when the stdout is redirected.");
}
}

/// <summary>
/// Throw exception if both standard output and error are redirected.
/// </summary>
Expand Down
Loading