Skip to content

Commit 78089ea

Browse files
committed
Initial implementation of MCP using API Controller classes
1 parent 62e4a3a commit 78089ea

File tree

8 files changed

+1156
-10
lines changed

8 files changed

+1156
-10
lines changed

demo/Program.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using NLWebNet;
1+
using NLWebNet.Extensions;
22
using NLWebNet.Models;
33

44
var builder = WebApplication.CreateBuilder(args);
@@ -7,6 +7,9 @@
77
builder.Services.AddRazorComponents()
88
.AddInteractiveServerComponents();
99

10+
// Add controllers for API endpoints
11+
builder.Services.AddControllers();
12+
1013
// Add NLWebNet services
1114
builder.Services.AddNLWebNet(options =>
1215
{
@@ -34,13 +37,19 @@
3437

3538
app.UseHttpsRedirection();
3639

40+
// Add NLWebNet middleware
41+
app.UseNLWebNet();
42+
3743
app.UseAntiforgery();
3844

3945
app.MapStaticAssets();
4046
app.MapRazorComponents<NLWebNet.Demo.Components.App>()
4147
.AddInteractiveServerRenderMode();
4248

43-
// Add NLWebNet endpoints
49+
// Map API controllers
50+
app.MapControllers();
51+
52+
// Add NLWebNet endpoints (optional, controllers are already mapped)
4453
app.MapNLWebNet();
4554

4655
app.Run();
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.Extensions.Logging;
4+
using NLWebNet.Models;
5+
using NLWebNet.Services;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Text.Json;
8+
9+
namespace NLWebNet.Controllers;
10+
11+
/// <summary>
12+
/// Controller for the NLWeb /ask endpoint.
13+
/// Implements the core NLWeb protocol for natural language queries.
14+
/// </summary>
15+
[ApiController]
16+
[Route("ask")]
17+
[Produces("application/json")]
18+
public class AskController : ControllerBase
19+
{
20+
private readonly INLWebService _nlWebService;
21+
private readonly ILogger<AskController> _logger;
22+
23+
public AskController(INLWebService nlWebService, ILogger<AskController> logger)
24+
{
25+
_nlWebService = nlWebService ?? throw new ArgumentNullException(nameof(nlWebService));
26+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
27+
}
28+
29+
/// <summary>
30+
/// Process a natural language query using the NLWeb protocol.
31+
/// Supports all three query modes: list, summarize, and generate.
32+
/// </summary>
33+
/// <param name="request">The NLWeb request containing the query and options</param>
34+
/// <param name="cancellationToken">Cancellation token</param>
35+
/// <returns>NLWeb response with results</returns>
36+
[HttpPost]
37+
[ProducesResponseType<NLWebResponse>(StatusCodes.Status200OK)]
38+
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
39+
[ProducesResponseType<ProblemDetails>(StatusCodes.Status500InternalServerError)]
40+
public async Task<IActionResult> ProcessQuery(
41+
[FromBody] NLWebRequest request,
42+
CancellationToken cancellationToken = default)
43+
{
44+
try
45+
{
46+
// Validate the request
47+
if (request == null)
48+
{
49+
_logger.LogWarning("Received null request");
50+
return BadRequest(new ProblemDetails
51+
{
52+
Title = "Invalid Request",
53+
Detail = "Request body is required",
54+
Status = StatusCodes.Status400BadRequest
55+
});
56+
}
57+
58+
if (string.IsNullOrWhiteSpace(request.Query))
59+
{
60+
_logger.LogWarning("Received request with empty query");
61+
return BadRequest(new ProblemDetails
62+
{
63+
Title = "Invalid Query",
64+
Detail = "Query parameter is required and cannot be empty",
65+
Status = StatusCodes.Status400BadRequest
66+
});
67+
}
68+
69+
// Generate query ID if not provided
70+
if (string.IsNullOrEmpty(request.QueryId))
71+
{
72+
request.QueryId = Guid.NewGuid().ToString();
73+
_logger.LogDebug("Generated query ID: {QueryId}", request.QueryId);
74+
}
75+
76+
_logger.LogInformation("Processing NLWeb query: {QueryId}, Mode: {Mode}, Query: {Query}",
77+
request.QueryId, request.Mode, request.Query);
78+
79+
// Check if streaming is requested
80+
if (request.Streaming == true)
81+
{
82+
return await ProcessStreamingQuery(request, cancellationToken);
83+
}
84+
85+
// Process non-streaming query
86+
var response = await _nlWebService.ProcessRequestAsync(request, cancellationToken);
87+
88+
_logger.LogInformation("Successfully processed query {QueryId} with {ResultCount} results",
89+
response.QueryId, response.Results?.Count ?? 0);
90+
91+
return Ok(response);
92+
}
93+
catch (ValidationException ex)
94+
{
95+
_logger.LogWarning(ex, "Validation error for query {QueryId}: {Message}",
96+
request?.QueryId, ex.Message);
97+
98+
return BadRequest(new ProblemDetails
99+
{
100+
Title = "Validation Error",
101+
Detail = ex.Message,
102+
Status = StatusCodes.Status400BadRequest
103+
});
104+
}
105+
catch (OperationCanceledException)
106+
{
107+
_logger.LogInformation("Query {QueryId} was cancelled", request?.QueryId);
108+
return StatusCode(StatusCodes.Status499ClientClosedRequest);
109+
}
110+
catch (Exception ex)
111+
{
112+
_logger.LogError(ex, "Error processing query {QueryId}: {Message}",
113+
request?.QueryId, ex.Message);
114+
115+
return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
116+
{
117+
Title = "Internal Server Error",
118+
Detail = "An error occurred while processing your request",
119+
Status = StatusCodes.Status500InternalServerError
120+
});
121+
}
122+
}
123+
124+
/// <summary>
125+
/// Process a natural language query via GET request with query parameters.
126+
/// This provides a simple interface for basic queries.
127+
/// </summary>
128+
/// <param name="query">The natural language query</param>
129+
/// <param name="mode">Query mode (list, summarize, generate)</param>
130+
/// <param name="site">Site filter (optional)</param>
131+
/// <param name="streaming">Enable streaming responses (default: true)</param>
132+
/// <param name="cancellationToken">Cancellation token</param>
133+
/// <returns>NLWeb response with results</returns>
134+
[HttpGet]
135+
[ProducesResponseType<NLWebResponse>(StatusCodes.Status200OK)]
136+
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
137+
public async Task<IActionResult> ProcessQueryGet(
138+
[FromQuery, Required] string query,
139+
[FromQuery] QueryMode mode = QueryMode.List,
140+
[FromQuery] string? site = null,
141+
[FromQuery] bool streaming = true,
142+
CancellationToken cancellationToken = default)
143+
{
144+
var request = new NLWebRequest
145+
{
146+
Query = query,
147+
Mode = mode,
148+
Site = site,
149+
Streaming = streaming,
150+
QueryId = Guid.NewGuid().ToString()
151+
};
152+
153+
return await ProcessQuery(request, cancellationToken);
154+
}
155+
156+
/// <summary>
157+
/// Process a streaming query using Server-Sent Events.
158+
/// </summary>
159+
private async Task<IActionResult> ProcessStreamingQuery(
160+
NLWebRequest request,
161+
CancellationToken cancellationToken)
162+
{
163+
_logger.LogDebug("Starting streaming response for query {QueryId}", request.QueryId);
164+
165+
// Set SSE headers
166+
Response.Headers.Append("Content-Type", "text/event-stream");
167+
Response.Headers.Append("Cache-Control", "no-cache");
168+
Response.Headers.Append("Connection", "keep-alive");
169+
Response.Headers.Append("Access-Control-Allow-Origin", "*");
170+
171+
try
172+
{
173+
await foreach (var response in _nlWebService.ProcessRequestStreamAsync(request, cancellationToken))
174+
{
175+
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
176+
{
177+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
178+
});
179+
180+
await Response.WriteAsync($"data: {json}\n\n", cancellationToken);
181+
await Response.Body.FlushAsync(cancellationToken);
182+
183+
_logger.LogDebug("Sent streaming chunk for query {QueryId}", request.QueryId);
184+
}
185+
186+
// Send end-of-stream marker
187+
await Response.WriteAsync("data: [DONE]\n\n", cancellationToken);
188+
await Response.Body.FlushAsync(cancellationToken);
189+
190+
_logger.LogInformation("Completed streaming response for query {QueryId}", request.QueryId);
191+
}
192+
catch (OperationCanceledException)
193+
{
194+
_logger.LogInformation("Streaming query {QueryId} was cancelled", request.QueryId);
195+
}
196+
catch (Exception ex)
197+
{
198+
_logger.LogError(ex, "Error during streaming for query {QueryId}: {Message}",
199+
request.QueryId, ex.Message);
200+
201+
// Send error as SSE
202+
var errorResponse = new { error = "An error occurred during streaming" };
203+
var errorJson = JsonSerializer.Serialize(errorResponse);
204+
await Response.WriteAsync($"data: {errorJson}\n\n", cancellationToken);
205+
}
206+
207+
return new EmptyResult();
208+
}
209+
}

0 commit comments

Comments
 (0)