Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.");
Comment thread
anushakolan marked this conversation as resolved.
}

/// <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 thread
anushakolan marked this conversation as resolved.
Outdated
HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest);
Comment thread
anushakolan marked this conversation as resolved.
Outdated
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 thread
anushakolan marked this conversation as resolved.
Outdated
}
Comment thread
anushakolan marked this conversation as resolved.

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