diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
index 2b48c37a83..387ea7427f 100644
--- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
+++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
@@ -19,7 +19,7 @@ internal static class McpServerConfiguration
///
/// Configures the MCP server with tool capabilities.
///
- internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services)
+ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services, string? instructions)
{
services.AddMcpServer()
.WithListToolsHandler((RequestContext request, CancellationToken ct) =>
@@ -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;
diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
index bc87602da9..c88cae148d 100644
--- a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
+++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
@@ -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;
}
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index e9182927ae..b6607391b7 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -2775,7 +2775,7 @@ public async Task TestGlobalFlagToEnableRestGraphQLAndMcpForHostedAndNonHostedEn
Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode, "The REST response is different from the expected result.");
// MCP request
- HttpStatusCode mcpResponseCode = await GetMcpResponse(client, configuration.Runtime.Mcp);
+ (HttpStatusCode mcpResponseCode, _) = await GetMcpResponse(client, configuration.Runtime.Mcp);
Assert.AreEqual(expectedStatusCodeForMcp, mcpResponseCode, "The MCP response is different from the expected result.");
}
@@ -2802,6 +2802,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.");
+ }
+
///
/// 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.
@@ -6226,13 +6264,13 @@ private static async Task GetGraphQLResponsePostConfigHydration(
return responseCode;
}
- ///
- /// Executing MCP POST requests against the engine until a non-503 error is received.
- ///
- /// Client used for request execution.
- /// ServiceUnavailable if service is not successfully hydrated with config,
- /// else the response code from the MCP request
- public static async Task GetMcpResponse(HttpClient httpClient, McpRuntimeOptions mcp)
+ ///
+ /// Executing MCP POST requests against the engine until a non-503 error is received.
+ ///
+ /// Client used for request execution.
+ /// MCP runtime options containing path configuration.
+ /// A tuple containing the HTTP status code and response body.
+ public static async Task<(HttpStatusCode StatusCode, string ResponseBody)> GetMcpResponse(HttpClient httpClient, McpRuntimeOptions mcp)
{
// Retry request RETRY_COUNT times in exponential increments to allow
// required services time to instantiate and hydrate permissions because
@@ -6242,6 +6280,8 @@ public static async Task GetMcpResponse(HttpClient httpClient, M
// but it is highly unlikely to be the case.
int retryCount = 0;
HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable;
+ string responseBody = string.Empty;
+
while (retryCount < RETRY_COUNT)
{
// Minimal MCP request (initialize) - valid JSON-RPC request.
@@ -6259,14 +6299,16 @@ public static async Task GetMcpResponse(HttpClient httpClient, M
clientInfo = new { name = "dab-test", version = "1.0.0" }
}
};
- HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path)
+
+ using 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 HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest);
responseCode = mcpResponse.StatusCode;
+ responseBody = await mcpResponse.Content.ReadAsStringAsync();
if (responseCode == HttpStatusCode.ServiceUnavailable || responseCode == HttpStatusCode.NotFound)
{
@@ -6278,7 +6320,85 @@ public static async Task GetMcpResponse(HttpClient httpClient, M
break;
}
- return responseCode;
+ return (responseCode, responseBody);
+ }
+
+ ///
+ /// Executes MCP initialize over HTTP and returns the parsed JSON response.
+ /// Reuses the core request/retry logic from GetMcpResponse.
+ ///
+ public static async Task GetMcpInitializeResponse(HttpClient httpClient, McpRuntimeOptions mcp)
+ {
+ (HttpStatusCode responseCode, string responseBody) = await GetMcpResponse(httpClient, mcp);
+
+ 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();
+ }
+
+ ///
+ /// Extracts JSON payload from SSE-formatted text.
+ /// SSE events can split JSON across multiple data: lines which should be concatenated.
+ ///
+ private static string ExtractJsonFromSsePayload(string ssePayload)
+ {
+ List eventDataLines = new();
+
+ static string GetJsonPayload(List 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');
+
+ // Empty line signals end of an SSE event
+ 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);
+ // SSE spec: if data starts with a space, strip one leading space
+ if (data.StartsWith(' '))
+ {
+ data = data.Substring(1);
+ }
+
+ eventDataLines.Add(data);
+ }
+ }
+
+ // Handle case where payload doesn't end with empty line
+ return GetJsonPayload(eventDataLines);
}
///