Skip to content

Commit f932833

Browse files
authored
Make AIShell an MCP client to expose MCP tools to its agents (#392)
This PR enables MCP client support in AIShell and also updates the openai-gpt agent to consume tools. For details, see the PR description at #392
1 parent 6e4edd5 commit f932833

File tree

22 files changed

+1055
-213
lines changed

22 files changed

+1055
-213
lines changed

shell/AIShell.Abstraction/AIShell.Abstraction.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77

88
<ItemGroup>
99
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
10+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
1011
</ItemGroup>
1112
</Project>

shell/AIShell.Abstraction/IShell.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Microsoft.Extensions.AI;
2+
13
namespace AIShell.Abstraction;
24

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

32+
/// <summary>
33+
/// Get available <see cref="AIFunction"/> instances for LLM to use.
34+
/// </summary>
35+
/// <returns></returns>
36+
Task<List<AIFunction>> GetAIFunctions();
37+
38+
/// <summary>
39+
/// Call an AI function.
40+
/// </summary>
41+
/// <param name="functionCall">A <see cref="FunctionCallContent"/> instance representing the function call request.</param>
42+
/// <param name="captureException">Whether or not to capture the exception thrown from calling the tool.</param>
43+
/// <param name="includeDetailedErrors">Whether or not to include the exception message to the message of the call result.</param>
44+
/// <param name="cancellationToken">The cancellation token to cancel the call.</param>
45+
/// <returns></returns>
46+
Task<FunctionResultContent> CallAIFunction(
47+
FunctionCallContent functionCall,
48+
bool captureException,
49+
bool includeDetailedErrors,
50+
CancellationToken cancellationToken);
51+
3052
// TODO:
3153
// - methods to run code: python, command-line, powershell, node-js.
3254
// - methods to communicate with shell client.

shell/AIShell.App/AIShell.App.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

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

shell/AIShell.Kernel/AIShell.Kernel.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
</PropertyGroup>
77

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

1313
<ItemGroup>

shell/AIShell.Kernel/Command/CommandRunner.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ internal CommandRunner(Shell shell)
3535
new RefreshCommand(),
3636
new RetryCommand(),
3737
new HelpCommand(),
38+
new McpCommand(),
3839
//new RenderCommand(),
3940
};
4041

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.CommandLine;
2+
using AIShell.Abstraction;
3+
4+
namespace AIShell.Kernel.Commands;
5+
6+
internal sealed class McpCommand : CommandBase
7+
{
8+
public McpCommand()
9+
: base("mcp", "Command for managing MCP servers and tools.")
10+
{
11+
this.SetHandler(ShowMCPData);
12+
13+
//var start = new Command("start", "Start an MCP server.");
14+
//var stop = new Command("stop", "Stop an MCP server.");
15+
//var server = new Argument<string>(
16+
// name: "server",
17+
// getDefaultValue: () => null,
18+
// description: "Name of an MCP server.").AddCompletions(AgentCompleter);
19+
20+
//start.AddArgument(server);
21+
//start.SetHandler(StartMcpServer, server);
22+
23+
//stop.AddArgument(server);
24+
//stop.SetHandler(StopMcpServer, server);
25+
}
26+
27+
private void ShowMCPData()
28+
{
29+
var shell = (Shell)Shell;
30+
var host = shell.Host;
31+
32+
if (shell.McpManager.McpServers.Count is 0)
33+
{
34+
host.WriteErrorLine("No MCP server is available.");
35+
return;
36+
}
37+
38+
host.RenderMcpServersAndTools(shell.McpManager);
39+
}
40+
}

shell/AIShell.Kernel/Host.cs

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
using System.Text;
33

44
using AIShell.Abstraction;
5+
using AIShell.Kernel.Mcp;
56
using Markdig.Helpers;
67
using Microsoft.PowerShell;
78
using Spectre.Console;
9+
using Spectre.Console.Json;
10+
using Spectre.Console.Rendering;
811

912
namespace AIShell.Kernel;
1013

@@ -175,7 +178,7 @@ public void RenderFullResponse(string response)
175178
/// <inheritdoc/>
176179
public void RenderTable<T>(IList<T> sources)
177180
{
178-
RequireStdoutOrStderr(operation: "render table");
181+
RequireStdout(operation: "render table");
179182
ArgumentNullException.ThrowIfNull(sources);
180183

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

203206
ArgumentNullException.ThrowIfNull(sources);
204207
ArgumentNullException.ThrowIfNull(elements);
@@ -240,7 +243,7 @@ public void RenderTable<T>(IList<T> sources, IList<IRenderElement<T>> elements)
240243
/// <inheritdoc/>
241244
public void RenderList<T>(T source)
242245
{
243-
RequireStdoutOrStderr(operation: "render list");
246+
RequireStdout(operation: "render list");
244247
ArgumentNullException.ThrowIfNull(source);
245248

246249
if (source is IDictionary<string, string> dict)
@@ -271,7 +274,7 @@ public void RenderList<T>(T source)
271274
/// <inheritdoc/>
272275
public void RenderList<T>(T source, IList<IRenderElement<T>> elements)
273276
{
274-
RequireStdoutOrStderr(operation: "render list");
277+
RequireStdout(operation: "render list");
275278

276279
ArgumentNullException.ThrowIfNull(source);
277280
ArgumentNullException.ThrowIfNull(elements);
@@ -313,7 +316,7 @@ public void RenderList<T>(T source, IList<IRenderElement<T>> elements)
313316
public void RenderDivider(string text, DividerAlignment alignment)
314317
{
315318
ArgumentException.ThrowIfNullOrEmpty(text);
316-
RequireStdoutOrStderr(operation: "render divider");
319+
RequireStdout(operation: "render divider");
317320

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

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

559-
AnsiConsole.WriteLine();
560-
AnsiConsole.Write(panel);
561-
AnsiConsole.WriteLine();
563+
ansiConsole.WriteLine();
564+
ansiConsole.Write(panel);
565+
ansiConsole.WriteLine();
566+
}
567+
568+
/// <summary>
569+
/// Render the MCP tool call request.
570+
/// </summary>
571+
/// <param name="tool">The MCP tool.</param>
572+
/// <param name="jsonArgs">The arguments in JSON form to be sent for the tool call.</param>
573+
internal void RenderToolCallRequest(McpTool tool, string jsonArgs)
574+
{
575+
RequireStdoutOrStderr(operation: "render tool call request");
576+
IAnsiConsole ansiConsole = _outputRedirected ? _stderrConsole : AnsiConsole.Console;
577+
578+
bool hasArgs = !string.IsNullOrEmpty(jsonArgs);
579+
IRenderable content = new Markup($"""
580+
581+
[bold]Run [olive]{tool.OriginalName}[/] from [olive]{tool.ServerName}[/] (MCP server)[/]
582+
583+
{tool.Description}
584+
585+
Input:{(hasArgs ? string.Empty : " <none>")}
586+
""");
587+
588+
if (hasArgs)
589+
{
590+
var json = new JsonText(jsonArgs)
591+
.MemberColor(Color.Aqua)
592+
.ColonColor(Color.White)
593+
.CommaColor(Color.White)
594+
.StringStyle(Color.Tan);
595+
596+
content = new Grid()
597+
.AddColumn(new GridColumn())
598+
.AddRow(content)
599+
.AddRow(json);
600+
}
601+
602+
var panel = new Panel(content)
603+
.Expand()
604+
.RoundedBorder()
605+
.Header("[green] Tool Call Request [/]")
606+
.BorderColor(Color.Grey);
607+
608+
ansiConsole.WriteLine();
609+
ansiConsole.Write(panel);
610+
FancyStreamRender.ConsoleUpdated();
611+
}
612+
613+
/// <summary>
614+
/// Render a table with information about available MCP servers and tools.
615+
/// </summary>
616+
/// <param name="mcpManager">The MCP manager instance.</param>
617+
internal void RenderMcpServersAndTools(McpManager mcpManager)
618+
{
619+
RequireStdout(operation: "render MCP servers and tools");
620+
621+
var toolTable = new Table()
622+
.LeftAligned()
623+
.SimpleBorder()
624+
.BorderColor(Color.Green);
625+
626+
toolTable.AddColumn("[green bold]Server[/]");
627+
toolTable.AddColumn("[green bold]Tool[/]");
628+
toolTable.AddColumn("[green bold]Description[/]");
629+
630+
List<(string name, string status, string info)> readyServers = null, startingServers = null, failedServers = null;
631+
foreach (var (name, server) in mcpManager.McpServers)
632+
{
633+
(int code, string status, string info) = server.IsInitFinished
634+
? server.Error is null
635+
? (1, "[green]\u2713 Ready[/]", string.Empty)
636+
: (-1, "[red]\u2717 Failed[/]", $"[red]{server.Error.Message.EscapeMarkup()}[/]")
637+
: (0, "[yellow]\u25CB Starting[/]", string.Empty);
638+
639+
var list = code switch
640+
{
641+
1 => readyServers ??= [],
642+
0 => startingServers ??= [],
643+
_ => failedServers ??= [],
644+
};
645+
646+
list.Add((name, status, info));
647+
}
648+
649+
if (startingServers is not null)
650+
{
651+
foreach (var (name, status, info) in startingServers)
652+
{
653+
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
654+
}
655+
}
656+
657+
if (failedServers is not null)
658+
{
659+
foreach (var (name, status, info) in failedServers)
660+
{
661+
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
662+
}
663+
}
664+
665+
if (readyServers is not null)
666+
{
667+
foreach (var (name, status, info) in readyServers)
668+
{
669+
if (toolTable.Rows is { Count: > 0 })
670+
{
671+
toolTable.AddEmptyRow();
672+
}
673+
674+
var server = mcpManager.McpServers[name];
675+
toolTable.AddRow($"[olive underline]{name}[/]", status, info);
676+
foreach (var item in server.Tools)
677+
{
678+
toolTable.AddRow(string.Empty, item.Key.EscapeMarkup(), item.Value.Description.EscapeMarkup());
679+
}
680+
}
681+
}
682+
683+
AnsiConsole.Write(toolTable);
562684
}
563685

564686
private static Spinner GetSpinner(SpinnerKind? kind)
@@ -583,6 +705,19 @@ private void RequireStdin(string operation)
583705
}
584706
}
585707

708+
/// <summary>
709+
/// Throw exception if standard output is redirected.
710+
/// </summary>
711+
/// <param name="operation">The intended operation.</param>
712+
/// <exception cref="InvalidOperationException">Throw the exception if stdout is redirected.</exception>
713+
private void RequireStdout(string operation)
714+
{
715+
if (_outputRedirected)
716+
{
717+
throw new InvalidOperationException($"Cannot {operation} when the stdout is redirected.");
718+
}
719+
}
720+
586721
/// <summary>
587722
/// Throw exception if both standard output and error are redirected.
588723
/// </summary>

0 commit comments

Comments
 (0)