using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.AspNetCore.Mvc;
namespace Sozsoft.Mcp.Controllers;
///
/// Uzak MCP istemcilerinin baglanti kesfi. Istemci /mcp'den 401 alir, WWW-Authenticate
/// basligindaki meta veri adresinden yetkilendirme sunucusunu ogrenir, kayit ucundan istemci
/// kimligini alir ve kullaniciyi platformun giris ekranina yonlendirir.
/// Adresler ve yanit bicimi RFC'lerle sabittir; bu yuzden ABP'nin konvansiyonel uclari yerine
/// acik rotali bir controller kullanilir.
///
[AllowAnonymous]
[IgnoreAntiforgeryToken]
[ApiExplorerSettings(IgnoreApi = true)]
public class McpOAuthController(
IOptions options,
IMcpOAuthAppService oAuthAppService) : AbpControllerBase
{
/// Korumali kaynak meta verisi (RFC 9728).
[HttpGet(McpConsts.ProtectedResourceMetadataPath)]
[HttpGet(McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath)]
public IActionResult GetProtectedResourceMetadata()
{
var value = options.Value;
return new JsonResult(new ProtectedResourceMetadata(
value.ResourceUrl,
[value.AuthorizationServer],
McpConsts.OAuthScopes,
["header"],
McpServerInfoDefaults.DisplayName));
}
/// Dinamik istemci kaydi (RFC 7591).
[HttpPost(McpConsts.RegistrationEndpointPath)]
public async Task RegisterAsync([FromBody] ClientRegistrationRequest request)
{
var result = await oAuthAppService.RegisterClientAsync(new McpClientRegistrationRequestDto
{
ClientName = request.ClientName,
RedirectUris = request.RedirectUris ?? []
});
if (result.Error is not null)
{
return new JsonResult(new ClientRegistrationError(result.Error, result.ErrorDescription))
{
StatusCode = StatusCodes.Status400BadRequest
};
}
return new JsonResult(new ClientRegistrationResponse(
result.ClientId!,
result.ClientIdIssuedAt,
result.ClientName,
result.RedirectUris,
result.GrantTypes,
result.ResponseTypes,
result.TokenEndpointAuthMethod,
result.Scope))
{
StatusCode = StatusCodes.Status201Created
};
}
// Protokol sozlesmeleri snake_case alan adlariyla sabittir; ABP'nin camelCase JSON
// politikasina birakilmaz.
public sealed record ClientRegistrationRequest(
[property: JsonPropertyName("client_name")] string? ClientName,
[property: JsonPropertyName("redirect_uris")] List? RedirectUris);
private sealed record ClientRegistrationResponse(
[property: JsonPropertyName("client_id")] string ClientId,
[property: JsonPropertyName("client_id_issued_at")] long ClientIdIssuedAt,
[property: JsonPropertyName("client_name")] string? ClientName,
[property: JsonPropertyName("redirect_uris")] IReadOnlyList RedirectUris,
[property: JsonPropertyName("grant_types")] IReadOnlyList GrantTypes,
[property: JsonPropertyName("response_types")] IReadOnlyList ResponseTypes,
[property: JsonPropertyName("token_endpoint_auth_method")] string TokenEndpointAuthMethod,
[property: JsonPropertyName("scope")] string? Scope);
private sealed record ClientRegistrationError(
[property: JsonPropertyName("error")] string Error,
[property: JsonPropertyName("error_description")] string? ErrorDescription);
private sealed record ProtectedResourceMetadata(
[property: JsonPropertyName("resource")] string Resource,
[property: JsonPropertyName("authorization_servers")] IReadOnlyList AuthorizationServers,
[property: JsonPropertyName("scopes_supported")] IReadOnlyList ScopesSupported,
[property: JsonPropertyName("bearer_methods_supported")] IReadOnlyList BearerMethodsSupported,
[property: JsonPropertyName("resource_name")] string ResourceName);
}