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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal static class McpServerConfiguration
/// <summary>
/// Configures the MCP server with tool capabilities.
/// </summary>
internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services)
internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services, string? instructions)
{
services.AddMcpServer()
.WithListToolsHandler((RequestContext<ListToolsRequestParams> request, CancellationToken ct) =>
Expand Down Expand Up @@ -93,6 +93,7 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se
options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION };
options.Capabilities ??= new();
options.Capabilities.Tools ??= new();
options.ServerInstructions = !string.IsNullOrWhiteSpace(instructions) ? instructions : null;
});

return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public static IServiceCollection AddDabMcpServer(this IServiceCollection service
// Register custom tools from configuration
RegisterCustomTools(services, runtimeConfig);

// Configure MCP server
services.ConfigureMcpServer();
// Configure MCP server and propagate runtime description to MCP initialize instructions.
services.ConfigureMcpServer(runtimeConfig.Runtime?.Mcp?.Description);

return services;
}
Expand Down
115 changes: 115 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,44 @@ public async Task TestGlobalFlagToEnableRestGraphQLAndMcpForHostedAndNonHostedEn
}
}

[TestMethod]
[TestCategory(TestCategory.MSSQL)]
public async Task TestMcpInitializeIncludesInstructionsFromRuntimeDescription()
{
const string MCP_INSTRUCTIONS = "Use SQL tools to query the database.";
const string CUSTOM_CONFIG = "custom-config-mcp-instructions.json";

TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT);

GraphQLRuntimeOptions graphqlOptions = new(Enabled: false);
RestRuntimeOptions restRuntimeOptions = new(Enabled: false);
McpRuntimeOptions mcpRuntimeOptions = new(Enabled: true, Description: MCP_INSTRUCTIONS);

SqlConnectionStringBuilder connectionStringBuilder = new(GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))
{
TrustServerCertificate = true
};

DataSource dataSource = new(DatabaseType.MSSQL,
connectionStringBuilder.ConnectionString, Options: null);

RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions);
File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson());

string[] args = new[]
{
$"--ConfigFileName={CUSTOM_CONFIG}"
};

using TestServer server = new(Program.CreateWebHostBuilder(args));
using HttpClient client = server.CreateClient();

JsonElement initializeResponse = await GetMcpInitializeResponse(client, configuration.Runtime.Mcp);
JsonElement result = initializeResponse.GetProperty("result");

Assert.AreEqual(MCP_INSTRUCTIONS, result.GetProperty("instructions").GetString(), "MCP initialize response should include instructions from runtime.mcp.description.");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cant we modify an existing test that checks InitializeResponse to check if the instructions are sent back in the output?

I expect there should be an existing test - otherwise we might be missing coverage.

}

/// <summary>
/// For mutation operations, both the respective operation(create/update/delete) + read permissions are needed to receive a valid response.
/// In this test, Anonymous role is configured with only create permission.
Expand Down Expand Up @@ -6284,6 +6322,83 @@ public static async Task<HttpStatusCode> GetMcpResponse(HttpClient httpClient, M
return responseCode;
}

/// <summary>
/// Executes MCP initialize over HTTP and returns the parsed JSON response.
/// </summary>
public static async Task<JsonElement> GetMcpInitializeResponse(HttpClient httpClient, McpRuntimeOptions mcp)
{
int retryCount = 0;
HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable;
string responseBody = string.Empty;

while (retryCount < RETRY_COUNT)
{
object payload = new
{
jsonrpc = "2.0",
id = 1,
method = "initialize",
@params = new
{
protocolVersion = "2025-03-26",
capabilities = new { },
clientInfo = new { name = "dab-test", version = "1.0.0" }
}
};

HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path)
{
Content = JsonContent.Create(payload)
};
mcpRequest.Headers.Add("Accept", "application/json, text/event-stream");

Comment on lines +6334 to +6354
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

GetMcpInitializeResponse duplicates the initialize request payload + retry loop logic already implemented in GetMcpResponse. This increases the chance the two helpers drift (e.g., protocolVersion/capabilities/headers/retry conditions). Consider extracting a shared helper for building/sending the MCP initialize request (and optionally returning the response body) to keep behavior consistent.

Copilot uses AI. Check for mistakes.
HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest);
Comment on lines +6349 to +6355
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Inside the retry loop, HttpRequestMessage/HttpResponseMessage are created repeatedly but never disposed. In long/parallel test runs this can retain resources longer than needed; prefer wrapping both in using (and reading content before disposal) within each iteration.

Suggested change
HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path)
{
Content = JsonContent.Create(payload)
};
mcpRequest.Headers.Add("Accept", "application/json, text/event-stream");
HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest);
using HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path)
{
Content = JsonContent.Create(payload)
};
mcpRequest.Headers.Add("Accept", "application/json, text/event-stream");
using HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest);

Copilot uses AI. Check for mistakes.
responseCode = mcpResponse.StatusCode;
responseBody = await mcpResponse.Content.ReadAsStringAsync();

if (responseCode == HttpStatusCode.ServiceUnavailable || responseCode == HttpStatusCode.NotFound)
{
retryCount++;
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount)));
continue;
}

break;
}

Assert.AreEqual(HttpStatusCode.OK, responseCode, "MCP initialize should return HTTP 200.");
Assert.IsFalse(string.IsNullOrWhiteSpace(responseBody), "MCP initialize response body should not be empty.");

// Depending on transport/content negotiation, initialize can return plain JSON
// or SSE-formatted text where JSON payload is carried in a data: line.
string payloadToParse = responseBody.TrimStart().StartsWith('{')
? responseBody
: ExtractJsonFromSsePayload(responseBody);

Assert.IsFalse(string.IsNullOrWhiteSpace(payloadToParse), "MCP initialize response did not contain a JSON payload.");

using JsonDocument responseDocument = JsonDocument.Parse(payloadToParse);
return responseDocument.RootElement.Clone();
}

private static string ExtractJsonFromSsePayload(string ssePayload)
{
foreach (string line in ssePayload.Split('\n'))
{
string trimmed = line.Trim();
if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
string data = trimmed.Substring("data:".Length).Trim();
if (!string.IsNullOrWhiteSpace(data) && data.StartsWith('{'))
{
return data;
}
}
}

return string.Empty;
Comment on lines +6386 to +6399
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

ExtractJsonFromSsePayload only returns a single data: line that starts with {. SSE events can legally split a JSON payload across multiple data: lines (which should be concatenated with newlines). Consider accumulating consecutive data: lines for the event and then parsing the combined string to avoid flaky parsing if the transport changes formatting/chunking.

Suggested change
foreach (string line in ssePayload.Split('\n'))
{
string trimmed = line.Trim();
if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
string data = trimmed.Substring("data:".Length).Trim();
if (!string.IsNullOrWhiteSpace(data) && data.StartsWith('{'))
{
return data;
}
}
}
return string.Empty;
List<string> eventDataLines = new();
static string GetJsonPayload(List<string> dataLines)
{
if (dataLines.Count == 0)
{
return string.Empty;
}
string combinedPayload = string.Join("\n", dataLines);
return !string.IsNullOrWhiteSpace(combinedPayload) && combinedPayload.TrimStart().StartsWith('{')
? combinedPayload
: string.Empty;
}
foreach (string rawLine in ssePayload.Split('\n'))
{
string line = rawLine.TrimEnd('\r');
if (string.IsNullOrWhiteSpace(line))
{
string jsonPayload = GetJsonPayload(eventDataLines);
if (!string.IsNullOrEmpty(jsonPayload))
{
return jsonPayload;
}
eventDataLines.Clear();
continue;
}
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
string data = line.Substring("data:".Length);
if (data.StartsWith(' '))
{
data = data.Substring(1);
}
eventDataLines.Add(data);
}
}
return GetJsonPayload(eventDataLines);

Copilot uses AI. Check for mistakes.
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copilot suggestions are quite valid


/// <summary>
/// Helper method to instantiate RuntimeConfig object needed for multiple create tests.
/// </summary>
Expand Down