parent
9f0beec63a
commit
9c597aad90
4 changed files with 533 additions and 37 deletions
102
src/mcpdotnet/Protocol/Transport/PasteArguments.cs
Normal file
102
src/mcpdotnet/Protocol/Transport/PasteArguments.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
// Copied from:
|
||||
// https://github.com/dotnet/runtime/blob/d2650b6ae7023a2d9d2c74c56116f1f18472ab04/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
|
||||
// and changed from using ValueStringBuilder to StringBuilder.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport
|
||||
{
|
||||
internal static partial class PasteArguments
|
||||
{
|
||||
internal static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length != 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
// Parsing rules for non-argv[0] arguments:
|
||||
// - Backslash is a normal character except followed by a quote.
|
||||
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
|
||||
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
|
||||
// - Parsing stops at first whitespace outside of quoted region.
|
||||
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
|
||||
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
|
||||
{
|
||||
// Simple case - no quoting or changes needed.
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(Quote);
|
||||
int idx = 0;
|
||||
while (idx < argument.Length)
|
||||
{
|
||||
char c = argument[idx++];
|
||||
if (c == Backslash)
|
||||
{
|
||||
int numBackSlash = 1;
|
||||
while (idx < argument.Length && argument[idx] == Backslash)
|
||||
{
|
||||
idx++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (idx == argument.Length)
|
||||
{
|
||||
// We'll emit an end quote after this so must double the number of backslashes.
|
||||
stringBuilder.Append(Backslash, numBackSlash * 2);
|
||||
}
|
||||
else if (argument[idx] == Quote)
|
||||
{
|
||||
// Backslashes will be followed by a quote. Must double the number of backslashes.
|
||||
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
|
||||
stringBuilder.Append(Quote);
|
||||
idx++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Backslash will not be followed by a quote, so emit as normal characters.
|
||||
stringBuilder.Append(Backslash, numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == Quote)
|
||||
{
|
||||
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
|
||||
// by another quote (which parses differently pre-2008 vs. post-2008.)
|
||||
stringBuilder.Append(Backslash);
|
||||
stringBuilder.Append(Quote);
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append(Quote);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsNoWhitespaceOrQuotes(string s)
|
||||
{
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
char c = s[i];
|
||||
if (char.IsWhiteSpace(c) || c == Quote)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private const char Quote = '\"';
|
||||
private const char Backslash = '\\';
|
||||
}
|
||||
}
|
||||
|
|
@ -64,13 +64,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
await Task.WhenAny(
|
||||
_connectionEstablished.Task,
|
||||
Task.Delay(_options.ConnectionTimeout, cancellationToken)
|
||||
);
|
||||
|
||||
if (!IsConnected)
|
||||
throw new TimeoutException("Failed to receive endpoint event");
|
||||
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken);
|
||||
}
|
||||
catch (McpTransportException)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Logging;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
|
|
@ -61,28 +63,31 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = OperatingSystem.IsWindows(),
|
||||
CreateNoWindow = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
|
||||
};
|
||||
|
||||
if (_options.Arguments != null)
|
||||
if (_options.Arguments is { Length: > 0 })
|
||||
{
|
||||
StringBuilder argsBuilder = new();
|
||||
foreach (var arg in _options.Arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(arg);
|
||||
PasteArguments.AppendArgument(argsBuilder, arg);
|
||||
}
|
||||
|
||||
startInfo.Arguments = argsBuilder.ToString();
|
||||
}
|
||||
|
||||
if (_options.EnvironmentVariables != null)
|
||||
{
|
||||
foreach (var (key, value) in _options.EnvironmentVariables)
|
||||
foreach (var entry in _options.EnvironmentVariables)
|
||||
{
|
||||
startInfo.Environment[key] = value;
|
||||
startInfo.Environment[entry.Key] = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.CreateProcessForTransport(EndpointName, _options.Command,
|
||||
string.Join(", ", startInfo.ArgumentList), string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)),
|
||||
startInfo.Arguments, string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)),
|
||||
startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString());
|
||||
|
||||
_process = new Process { StartInfo = startInfo };
|
||||
|
|
@ -102,9 +107,8 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_processStarted = true;
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
|
||||
// Start reading messages in the background
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None);
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
SetConnected(true);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
{
|
||||
_shutdownCts = new CancellationTokenSource();
|
||||
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None);
|
||||
|
||||
SetConnected(true);
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
using (Console.OpenStandardOutput())
|
||||
{
|
||||
await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken);
|
||||
await Console.Out.FlushAsync();
|
||||
await Console.Out.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_logger.TransportSentMessage(EndpointName, id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue