Added MCp support
This commit is contained in:
@@ -16,6 +16,8 @@ The PFM platform is constructed using **Clean Architecture** and **Domain-Driven
|
|||||||
│ PostFundManagement.ConsoleUI │ PostFundManagement.PartnerUI │
|
│ PostFundManagement.ConsoleUI │ PostFundManagement.PartnerUI │
|
||||||
│ (Internal Staff & Governance) │ (External Recipient Portal) │
|
│ (Internal Staff & Governance) │ (External Recipient Portal) │
|
||||||
│ React 19 + TypeScript + Vite │ React 19 + WebGL (OGL) + Vite │
|
│ React 19 + TypeScript + Vite │ React 19 + WebGL (OGL) + Vite │
|
||||||
|
│ │ │
|
||||||
|
│ PostFundManagement.Mcp (External MCP Server Host - HTTP/SSE & Stdio) │
|
||||||
└───────────────────────────────────┬────────────────────────────────────┘
|
└───────────────────────────────────┬────────────────────────────────────┘
|
||||||
│ HTTPS / REST (v1/api/*) + OIDC PKCE
|
│ HTTPS / REST (v1/api/*) + OIDC PKCE
|
||||||
┌───────────────────────────────────▼────────────────────────────────────┐
|
┌───────────────────────────────────▼────────────────────────────────────┐
|
||||||
@@ -316,3 +318,24 @@ The background job runner powers four essential operational workloads:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Model Context Protocol (MCP) Architectural Specification
|
||||||
|
|
||||||
|
### 11.1 Architectural Shift & Philosophy
|
||||||
|
The PFM platform integrates the **Model Context Protocol (MCP)** standard (v2.3.0) to expose domain capabilities both internally and externally while preventing token exhaustion and eliminating network call overhead:
|
||||||
|
|
||||||
|
1. **Dual Realisation Pattern**:
|
||||||
|
* **In-App MCP (`PostFundManagement.Api`)**: Internal features, handlers, and workflows obtain in-process access to MCP tooling via `IInAppMcpClient`. Calls are executed directly against scoped services with zero network latency, zero serialization overhead, and no external web requests.
|
||||||
|
* **External MCP Server (`PostFundManagement.Mcp`)**: A dedicated standalone presentation host exposes the exact same tools over standard external transports (HTTP/SSE at `/mcp` and command-line Stdio via `--stdio`) for external LLMs, agentic workbenches, and client IDEs.
|
||||||
|
|
||||||
|
2. **Application.Services Proxying Pattern**:
|
||||||
|
All tools reside in `PostFundManagement.Application/Mcp` and proxy established `Application.Services` interfaces (`IGrantsService`, `IProjectService`, `IExecutionService`, `IGovernanceService`, `IEvidenceService`, `ICommunicationsService`, `IUserService`). This ensures:
|
||||||
|
* **Single Source of Truth (BR-005)**: Business validation, entity mapping, and audit logging happen identically whether called from an HTTP endpoint, an internal in-app MCP consumer, or an external MCP server.
|
||||||
|
* **Domain Groupings**: Tools are partitioned by service domains (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`), providing clean tool namespaces (e.g., `grants_create_award`, `execution_approve_disbursement`).
|
||||||
|
* **Token Usage Optimization**: Responses are wrapped in concise `McpToolResult<T>` records, filtering out internal framework metadata and returning concise JSON structures to minimize LLM context window consumption.
|
||||||
|
|
||||||
|
3. **Current Agentic Boundary**:
|
||||||
|
The platform establishes the full tooling layer and in-app client contracts; no autonomous agents are defined in this release, preserving clear architectural boundaries for scheduled future agent implementations.
|
||||||
|
|||||||
@@ -85,6 +85,16 @@
|
|||||||
<Using Include="MimeKit" />
|
<Using Include="MimeKit" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Domain-specific Usings (Application and downstream projects excluding Infrastructure where Claim/Activity/TokenRequest collide) -->
|
||||||
|
<ItemGroup Condition="'$(MSBuildProjectName)' == 'PostFundManagement.Application'">
|
||||||
|
<Using Include="ModelContextProtocol.Server" />
|
||||||
|
<Using Include="System.ComponentModel" />
|
||||||
|
<Using Include="System.Collections.Concurrent" />
|
||||||
|
<Using Include="System.IO.Pipelines" />
|
||||||
|
<Using Include="ModelContextProtocol.Client" />
|
||||||
|
<Using Include="ModelContextProtocol.Protocol" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Domain-specific Usings (Domain and downstream projects excluding Infrastructure where Claim/Activity/TokenRequest collide) -->
|
<!-- Domain-specific Usings (Domain and downstream projects excluding Infrastructure where Claim/Activity/TokenRequest collide) -->
|
||||||
<ItemGroup Condition="'$(MSBuildProjectName)' == 'PostFundManagement.Domain'">
|
<ItemGroup Condition="'$(MSBuildProjectName)' == 'PostFundManagement.Domain'">
|
||||||
<Using Include="System.Security.Claims" />
|
<Using Include="System.Security.Claims" />
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
<PackageVersion Include="Polly" Version="8.8.1" />
|
<PackageVersion Include="Polly" Version="8.8.1" />
|
||||||
<PackageVersion Include="Polly.Extensions" Version="8.8.1" />
|
<PackageVersion Include="Polly.Extensions" Version="8.8.1" />
|
||||||
|
|
||||||
|
<!-- Model Context Protocol (MCP) -->
|
||||||
|
<PackageVersion Include="ModelContextProtocol" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="ModelContextProtocol.Core" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.3.0" />
|
||||||
|
|
||||||
<!-- API, Web & Documentation -->
|
<!-- API, Web & Documentation -->
|
||||||
<PackageVersion Include="Asp.Versioning.Abstractions" Version="10.2.1" />
|
<PackageVersion Include="Asp.Versioning.Abstractions" Version="10.2.1" />
|
||||||
<PackageVersion Include="Asp.Versioning.Http" Version="10.2.3" />
|
<PackageVersion Include="Asp.Versioning.Http" Version="10.2.3" />
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ PostFundManagement/
|
|||||||
├── PostFundManagement.Domain/ # Core entities, enums, value objects, domain logic
|
├── PostFundManagement.Domain/ # Core entities, enums, value objects, domain logic
|
||||||
├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL (Npgsql), Migrations, Data Protection
|
├── PostFundManagement.Infrastructure/ # EF Core 10, PostgreSQL (Npgsql), Migrations, Data Protection
|
||||||
├── PostFundManagement.Application/ # CQRS Handlers, Mediator, DTOs, Validations, S3, Email, Quartz
|
├── PostFundManagement.Application/ # CQRS Handlers, Mediator, DTOs, Validations, S3, Email, Quartz
|
||||||
├── PostFundManagement.Api/ # Minimal APIs / Carter endpoints, Auth, Swagger/Scalar, Versioning
|
├── PostFundManagement.Api/ # Minimal APIs / Carter endpoints, Auth, Swagger/Scalar, In-App MCP Consumer
|
||||||
|
├── PostFundManagement.Mcp/ # Dedicated Model Context Protocol (MCP) Server Host (HTTP/SSE & Stdio Transports)
|
||||||
├── PostFundManagement.Jobs/ # Dedicated Quartz.NET Background Worker Host (Recurring & One-Time Execution)
|
├── PostFundManagement.Jobs/ # Dedicated Quartz.NET Background Worker Host (Recurring & One-Time Execution)
|
||||||
├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet)
|
├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet)
|
||||||
├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4)
|
├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4)
|
||||||
@@ -86,7 +87,8 @@ PostFundManagement/
|
|||||||
| **`PostFundManagement.Domain`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Humanizer` 3.0.10, `Hashids.net` 1.7.0, `OpenTelemetry` 1.18.0, `Quartz` 4.1.0, `FluentResults` 4.0.0, `Mediator.Abstractions` 3.0.2, `AWSSDK.S3` 4.0.103.3, `MailKit`/`MimeKit` 4.18.0 | Contains domain entities, aggregate roots, value objects, domain enums, constant definitions, and interfaces. Clean of presentation concerns. |
|
| **`PostFundManagement.Domain`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Humanizer` 3.0.10, `Hashids.net` 1.7.0, `OpenTelemetry` 1.18.0, `Quartz` 4.1.0, `FluentResults` 4.0.0, `Mediator.Abstractions` 3.0.2, `AWSSDK.S3` 4.0.103.3, `MailKit`/`MimeKit` 4.18.0 | Contains domain entities, aggregate roots, value objects, domain enums, constant definitions, and interfaces. Clean of presentation concerns. |
|
||||||
| **`PostFundManagement.Infrastructure`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Duende.AccessTokenManagement` 4.2.0, EF Core Tools & Design | Houses `PostFundManagementDbContext`, EF Core fluent configurations, entity-to-table mappings, PostgreSQL schema definitions, multitenancy filters, and migrations. |
|
| **`PostFundManagement.Infrastructure`** | `net10.0` | `Microsoft.EntityFrameworkCore` 10.0.12, `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3, `Duende.AccessTokenManagement` 4.2.0, EF Core Tools & Design | Houses `PostFundManagementDbContext`, EF Core fluent configurations, entity-to-table mappings, PostgreSQL schema definitions, multitenancy filters, and migrations. |
|
||||||
| **`PostFundManagement.Application`** | `net10.0` | `Mediator.SourceGenerator` / `Mediator.Abstractions` 3.0.2, `FluentResults` 4.0.0, `Microsoft.Extensions.Caching.Hybrid` 10.10.0, `Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12, `Refit` 15.2.0, `Duende.AccessTokenManagement.OpenIdConnect` 4.2.0, `Quartz` 4.1.0, `AWSSDK.S3` | Implements CQRS command/query handlers, domain validators, S3 object storage clients (Garage S3), background jobs (Quartz), notification dispatchers, and business logic. |
|
| **`PostFundManagement.Application`** | `net10.0` | `Mediator.SourceGenerator` / `Mediator.Abstractions` 3.0.2, `FluentResults` 4.0.0, `Microsoft.Extensions.Caching.Hybrid` 10.10.0, `Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12, `Refit` 15.2.0, `Duende.AccessTokenManagement.OpenIdConnect` 4.2.0, `Quartz` 4.1.0, `AWSSDK.S3` | Implements CQRS command/query handlers, domain validators, S3 object storage clients (Garage S3), background jobs (Quartz), notification dispatchers, and business logic. |
|
||||||
| **`PostFundManagement.Api`** | `net10.0` | `Microsoft.AspNetCore.OpenApi` 10.0.12, `Scalar.AspNetCore` 2.17.4, `Asp.Versioning.Http` 10.2.3, `IdentityModel.AspNetCore` 4.3.0, `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.12, `AspNetCore.HealthChecks.NpgSql` 9.0.0, `AspNetCore.HealthChecks.UI` | Minimal API endpoint definitions divided by business slices (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Handles routing, auth policies, error problem details, and OpenAPI metadata. |
|
| **`PostFundManagement.Api`** | `net10.0` | `Microsoft.AspNetCore.OpenApi` 10.0.12, `Scalar.AspNetCore` 2.17.4, `Asp.Versioning.Http` 10.2.3, `IdentityModel.AspNetCore` 4.3.0, `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.12, `ModelContextProtocol` 2.3.0, `AspNetCore.HealthChecks.NpgSql` 9.0.0, `AspNetCore.HealthChecks.UI` | Minimal API endpoint definitions divided by business slices (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Features direct in-app MCP tooling access (`IInAppMcpClient`) for in-process tool execution without external network calls. Handles routing, auth policies, error problem details, and OpenAPI metadata. |
|
||||||
|
| **`PostFundManagement.Mcp`** | `net10.0` | `ModelContextProtocol.AspNetCore` 2.3.0, `ModelContextProtocol` 2.3.0, `ModelContextProtocol.Core` 2.3.0, `AspNetCore.HealthChecks.NpgSql` 9.0.0, `AspNetCore.HealthChecks.UI` | Dedicated Model Context Protocol (MCP) Server host. Realises and serves tools defined in `Application.Mcp` externally over HTTP/SSE (`/mcp`) and Stdio (`--stdio`) for external LLMs, agent frameworks, and IDEs. |
|
||||||
| **`PostFundManagement.Jobs`** | `net10.0` | `Quartz` 4.6.2, `Quartz.Extensions.DependencyInjection` 4.6.2, `Quartz.Serialization.SystemTextJson` 4.6.2, `Mediator.Abstractions` 3.0.2 | Dedicated background execution host. Executes recurring Quartz jobs (registered on startup by `JobsWorker`), processes asynchronous domain events (`IEvent`), and executes jobs concurrently (`MaxConcurrency = 5`). |
|
| **`PostFundManagement.Jobs`** | `net10.0` | `Quartz` 4.6.2, `Quartz.Extensions.DependencyInjection` 4.6.2, `Quartz.Serialization.SystemTextJson` 4.6.2, `Mediator.Abstractions` 3.0.2 | Dedicated background execution host. Executes recurring Quartz jobs (registered on startup by `JobsWorker`), processes asynchronous domain events (`IEvent`), and executes jobs concurrently (`MaxConcurrency = 5`). |
|
||||||
| **`PostFundManagement.Tests`** | `net10.0` | `xunit` 2.9.3, `xunit.runner.visualstudio` 3.1.4, `Microsoft.NET.Test.Sdk` 17.14.1, `coverlet.collector` 6.0.4 | Unit and integration test coverage for domain rules, CQRS handlers, financial calculation models, and gate condition validations. |
|
| **`PostFundManagement.Tests`** | `net10.0` | `xunit` 2.9.3, `xunit.runner.visualstudio` 3.1.4, `Microsoft.NET.Test.Sdk` 17.14.1, `coverlet.collector` 6.0.4 | Unit and integration test coverage for domain rules, CQRS handlers, financial calculation models, and gate condition validations. |
|
||||||
|
|
||||||
@@ -146,6 +148,68 @@ PostFundManagement/
|
|||||||
3. *Email Notification Processing*: Batches and dispatches pending notification entries (`Platform.Email` / `NotificationPlatform.Email`) via MailKit/MimeKit SMTP and records delivery telemetry (`FR-M25-001`).
|
3. *Email Notification Processing*: Batches and dispatches pending notification entries (`Platform.Email` / `NotificationPlatform.Email`) via MailKit/MimeKit SMTP and records delivery telemetry (`FR-M25-001`).
|
||||||
4. *Future Jobs*: Seamlessly supports future scheduled workloads (e.g. drawdown reconciliations, compliance expiries, portfolio aggregations) simply by adding `IEvent` handlers.
|
4. *Future Jobs*: Seamlessly supports future scheduled workloads (e.g. drawdown reconciliations, compliance expiries, portfolio aggregations) simply by adding `IEvent` handlers.
|
||||||
|
|
||||||
|
### 2.5 Model Context Protocol (MCP) Architecture (Internal In-App & External Server Topology)
|
||||||
|
The platform establishes a dual-channel Model Context Protocol (MCP) architecture on **.NET 10** using `ModelContextProtocol.AspNetCore` (v2.3.0):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MCP TOOLING CONSUMERS │
|
||||||
|
│ │
|
||||||
|
│ [External AI Agents / Claude / Cursor] [Internal PFM API Flows] │
|
||||||
|
│ │ │ │
|
||||||
|
│ HTTP / SSE │ Stdio (--stdio) │ In-Process │
|
||||||
|
│ (/mcp) │ │ Direct Call │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ PostFundManagement.Mcp │ │PostFundManagement. │ │
|
||||||
|
│ │ (Dedicated Server Host) │ │Api (In-App Consumer)│ │
|
||||||
|
│ └────────────┬────────────┘ └──────────┬──────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Direct DI / Component Reference │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PostFundManagement.Application/Mcp │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Grants] [Projects] [Execution] [Governance] │ │
|
||||||
|
│ │ [Evidence] [Communications][Users] │ │
|
||||||
|
│ │ Proxying: Application.Services.* │ │
|
||||||
|
│ │ InApp Client: IInAppMcpClient / InAppMcpClient │ │
|
||||||
|
│ └──────────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Application Domain Services │ │
|
||||||
|
│ │ IGrantsService, IProjectService, IExecutionService, ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Tool Definitions Centralized in `Application.Mcp`**:
|
||||||
|
- All MCP tools are defined in `PostFundManagement.Application` under the `Mcp` directory.
|
||||||
|
- Tools are grouped strictly according to `Application.Services` boundaries:
|
||||||
|
* `GrantsMcpTools`: Proxies `IGrantsService` (Awards M01, Contracts M02, Budgets M05).
|
||||||
|
* `ProjectsMcpTools`: Proxies `IProjectService` (WBS Activities M04, Milestones M07, Beneficiaries M13, Changes M15, Issues M16, Site Visits M17).
|
||||||
|
* `ExecutionMcpTools`: Proxies `IExecutionService` (Disbursements M06, Risk Register M09, Gating Rules BR-003).
|
||||||
|
* `GovernanceMcpTools`: Proxies `IGovernanceService` (Organisations M03, Portfolios, Instruments BR-006, Tenants, Dashboards M20/M22).
|
||||||
|
* `EvidenceMcpTools`: Proxies `IEvidenceService` (Audit Artifacts BR-004, M&E Indicators M08, Target/Actual Variance BR-009).
|
||||||
|
* `CommunicationsMcpTools`: Proxies `ICommunicationsService` (Notifications M25, Onboarding Invites M03).
|
||||||
|
* `UsersMcpTools`: Proxies `IUserService` (Identity accounts, RBAC roles, security claims).
|
||||||
|
|
||||||
|
2. **In-App MCP Consumer in `PostFundManagement.Api` (Zero Web Hops)**:
|
||||||
|
- `PostFundManagement.Api` accesses tools in-process via `IInAppMcpClient` registered by `AddApplicationMcpTools()`.
|
||||||
|
- Resolves and executes tool methods directly against scoped domain services without issuing HTTP network calls to an external MCP server.
|
||||||
|
- Provides optional in-process protocol compatibility via `CreateProtocolClientAsync()` using duplex in-memory streams.
|
||||||
|
- *Design Boundary*: No autonomous AI agents are defined in the solution at this time; the tooling layer and in-app consumer interfaces provide the underlying foundation for future agentic orchestration.
|
||||||
|
|
||||||
|
3. **External MCP Server in `PostFundManagement.Mcp`**:
|
||||||
|
- Dedicated Web/CLI host running .NET 10.
|
||||||
|
- Exposes tools externally via HTTP/SSE (`/mcp`) for remote agents, orchestrators, and platforms.
|
||||||
|
- Supports local Stdio transport (`--stdio`) for desktop agents, Claude Desktop, and IDE tools.
|
||||||
|
- Docker containerized (`pfm-mcp` service on port 8079).
|
||||||
|
|
||||||
|
4. **Token Usage Optimization**:
|
||||||
|
- Tools return standardized, lightweight `McpToolResult<T>` and `McpToolResult` models.
|
||||||
|
- Strips internal FluentResults metadata and complex execution graphs to ensure minimal prompt token bloat for consuming LLMs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Master Business Rules (BR-001 to BR-010)
|
## 3. Master Business Rules (BR-001 to BR-010)
|
||||||
|
|||||||
@@ -31,9 +31,10 @@
|
|||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- API Documentation -->
|
<!-- API Documentation & Model Context Protocol -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" />
|
<PackageReference Include="Scalar.AspNetCore" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
|
|||||||
builder.Services.AddApiServices(builder.Configuration);
|
builder.Services.AddApiServices(builder.Configuration);
|
||||||
builder.Services.AddSharedServices();
|
builder.Services.AddSharedServices();
|
||||||
builder.Services.AddRegisterServicesWithCaching(builder.Configuration);
|
builder.Services.AddRegisterServicesWithCaching(builder.Configuration);
|
||||||
|
builder.Services.AddApplicationMcpTools();
|
||||||
builder.Services.AddEndpointAndServiceCaching(builder.Configuration);
|
builder.Services.AddEndpointAndServiceCaching(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.AddMediator();
|
builder.Services.AddMediator();
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ModelContextProtocol.Client;
|
||||||
|
using PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Abstractions.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-app MCP access interface providing in-process tool discovery and execution without network or HTTP hops.
|
||||||
|
/// </summary>
|
||||||
|
public interface IInAppMcpClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lists all available MCP tools defined across Application.Services groupings.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<McpToolDefinition> GetAvailableTools();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly executes an in-app MCP tool by name using the current service scope.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpToolExecutionResult> ExecuteToolAsync(
|
||||||
|
string toolName,
|
||||||
|
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fully functional standard ModelContextProtocol McpClient connected in-process over in-memory streams.
|
||||||
|
/// </summary>
|
||||||
|
Task<McpClient> CreateProtocolClientAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Application.Services.Communications;
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Application.Services.Evidence;
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Application.Services.Governance;
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Application.Services.Grants;
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Application.Services.Projects;
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using PostFundManagement.Domain;
|
|||||||
using PostFundManagement.Domain.Attributes;
|
using PostFundManagement.Domain.Attributes;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Models.Security;
|
using PostFundManagement.Domain.Models.Security;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
using Role = PostFundManagement.Domain.Models.Security.Role;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain.Errors;
|
using PostFundManagement.Domain.Errors;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Abstractions.Services;
|
namespace PostFundManagement.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Mcp;
|
||||||
|
using PostFundManagement.Application.Mcp;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
public static class Mcp
|
||||||
|
{
|
||||||
|
public static Records<T> ToMcpResult<T>(this Result<T> result)
|
||||||
|
{
|
||||||
|
if (result.IsSuccess)
|
||||||
|
return Records<T>.Ok(result.Value);
|
||||||
|
|
||||||
|
var errorMsg = string.Join("; ", result.Errors.Select(e => e.Message));
|
||||||
|
var errorList = result.Errors.Select(e => e.Message).ToList();
|
||||||
|
|
||||||
|
return Records<T>.Fail(errorMsg, errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static McpToolResult ToMcpResult(this Result result, string successMessage = "Operation completed successfully.")
|
||||||
|
{
|
||||||
|
if (result.IsSuccess)
|
||||||
|
return McpToolResult.Ok(successMessage);
|
||||||
|
|
||||||
|
var errorMsg = string.Join("; ", result.Errors.Select(e => e.Message));
|
||||||
|
var errorList = result.Errors.Select(e => e.Message).ToList();
|
||||||
|
|
||||||
|
return McpToolResult.Fail(errorMsg, errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddApplicationMcpTools(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<GrantsMcpTools>();
|
||||||
|
services.AddScoped<ProjectsMcpTools>();
|
||||||
|
services.AddScoped<ExecutionMcpTools>();
|
||||||
|
services.AddScoped<GovernanceMcpTools>();
|
||||||
|
services.AddScoped<EvidenceMcpTools>();
|
||||||
|
services.AddScoped<CommunicationsMcpTools>();
|
||||||
|
services.AddScoped<UsersMcpTools>();
|
||||||
|
|
||||||
|
services.AddScoped<IInAppMcpClient, InAppMcpClient>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IMcpServerBuilder WithApplicationMcpTools(this IMcpServerBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithTools<GrantsMcpTools>()
|
||||||
|
.WithTools<ProjectsMcpTools>()
|
||||||
|
.WithTools<ExecutionMcpTools>()
|
||||||
|
.WithTools<GovernanceMcpTools>()
|
||||||
|
.WithTools<EvidenceMcpTools>()
|
||||||
|
.WithTools<CommunicationsMcpTools>()
|
||||||
|
.WithTools<UsersMcpTools>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Communications;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Communications, Onboarding Invites & Notification Services (Module M25, Recipient Onboarding M03).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class CommunicationsMcpTools(ICommunicationsService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "communications_get_notifications", Title = "Get Organisation Notifications", ReadOnly = true)]
|
||||||
|
[Description("Retrieves notifications and alerts dispatched to an organisation.")]
|
||||||
|
public async Task<Records<Notification[]>> GetNotificationsAsync([Description("Organisation hash identifier")] string organisationIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetNotificationsAsync(organisationIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_get_user_notifications", Title = "Get User Notifications", ReadOnly = true)]
|
||||||
|
[Description("Retrieves pending notifications and task reminders for a specific user.")]
|
||||||
|
public async Task<Records<Notification[]>> GetUserNotificationsAsync([Description("Target user identifier")] Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetUserNotificationsAsync(userId, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_create_notification", Title = "Dispatch Notification", Destructive = false)]
|
||||||
|
[Description("Creates and queues a system, in-app, or email notification for delivery (FR-M25-001).")]
|
||||||
|
public async Task<McpToolResult> CreateNotificationAsync([Description("Notification creation request")] CreateNotificationRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateNotificationAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Notification successfully created and queued for delivery.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_update_notification", Title = "Update Notification Status", Destructive = false)]
|
||||||
|
[Description("Updates the delivery or read status of a notification.")]
|
||||||
|
public async Task<McpToolResult> UpdateNotificationAsync([Description("Notification update request")] UpdateNotificationRequest request, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateNotificationAsync(request, cancellationToken)).ToMcpResult("Notification status successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_send_invite", Title = "Send Onboarding Invite", Destructive = false)]
|
||||||
|
[Description("Sends an invitation email and onboarding link to an external grantee or partner organisation user (Module M03).")]
|
||||||
|
public async Task<Records<Invite>> SendInviteAsync([Description("Invite request parameters")] InviteRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.SendInviteAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_get_invites", Title = "Get Onboarding Invites", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of sent onboarding invitations and their verification states.")]
|
||||||
|
public async Task<Records<Invite[]>> GetInvitesAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetInvitesAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "communications_get_applicant_data", Title = "Get Applicant Intake Data", ReadOnly = true)]
|
||||||
|
[Description("Retrieves captured applicant and grantee profile data for onboarding validation.")]
|
||||||
|
public async Task<Records<ApplicantData[]>> GetApplicantDataAsync([Description("Applicant user identifier")] Guid applicantUserId, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetApplicantDataAsync(applicantUserId, cancellationToken)).ToMcpResult();
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Evidence;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Evidence, Indicators & Verification Services (BR-004 Evidence-Based Performance, M07 Milestones, M08 M&E Indicators).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class EvidenceMcpTools(IEvidenceService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "evidence_get_files", Title = "Get Uploaded Evidence Records", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of uploaded evidence documents and audit artifacts (BR-004, Module M07).")]
|
||||||
|
public async Task<Records<Evidence[]>> GetEvidenceFilesAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetEvidenceFilesAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_evidence", Title = "Get Evidence by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves metadata, verification status, and hash verification for an evidence document.")]
|
||||||
|
public async Task<Records<Evidence>> GetEvidenceAsync([Description("Evidence hash identifier")] string evidenceIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetEvidenceAsync(evidenceIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_milestone_evidence", Title = "Get Milestone Evidence", ReadOnly = true)]
|
||||||
|
[Description("Retrieves all evidence documents uploaded to substantiate a specific milestone.")]
|
||||||
|
public async Task<Records<Evidence[]>> GetMilestoneEvidenceAsync([Description("Milestone hash identifier")] string milestoneIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_project_evidence", Title = "Get Project Evidence", ReadOnly = true)]
|
||||||
|
[Description("Retrieves all evidence documents associated with a project across all its milestones and indicators.")]
|
||||||
|
public async Task<Records<Evidence[]>> GetProjectEvidenceAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_url", Title = "Get Evidence Document URL", ReadOnly = true)]
|
||||||
|
[Description("Generates a secure presigned download URL for an uploaded evidence artifact in object storage.")]
|
||||||
|
public async Task<Records<string>> GetEvidenceUrlAsync([Description("Evidence hash identifier")] string evidenceIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_indicators", Title = "Get M&E Indicators", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of M&E indicators measuring project results hierarchy (Module M08, BR-008, BR-009).")]
|
||||||
|
public async Task<Records<Indicator[]>> GetIndicatorsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetIndicatorsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_get_indicator", Title = "Get Indicator by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves baseline, target, actual values, and variance calculation for an indicator (BR-009).")]
|
||||||
|
public async Task<Records<Indicator>> GetIndicatorAsync([Description("Indicator hash identifier")] string indicatorIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetIndicatorAsync(indicatorIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_create_indicator", Title = "Create Performance Indicator", Destructive = false)]
|
||||||
|
[Description("Creates an M&E indicator measuring Output, Outcome, or Impact with baseline and targets (Module M08, BR-008).")]
|
||||||
|
public async Task<McpToolResult> CreateIndicatorAsync([Description("Indicator creation request")] CreateIndicatorRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateIndicatorAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Indicator successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_record_actual_amount", Title = "Record Indicator Actual Value", Destructive = false)]
|
||||||
|
[Description("Records achieved actual metric and cost for an indicator, recalculating variance against target (BR-009).")]
|
||||||
|
public async Task<McpToolResult> RecordIndicatorActualAmountAsync([Description("Indicator hash identifier")] string indicatorIdHash, [Description("Actual numerical value achieved")] decimal amount, [Description("Optional actual financial cost incurred")] decimal? actualCost = null,
|
||||||
|
[Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, actualCost, userId ?? SystemUser, cancellationToken)).ToMcpResult("Indicator actual value successfully recorded.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_create_milestone", Title = "Create Milestone with Gating Criteria", Destructive = false)]
|
||||||
|
[Description("Defines a project milestone with verification evidence rules gating tranche disbursements (Module M07, BR-003).")]
|
||||||
|
public async Task<McpToolResult> CreateMilestoneAsync([Description("Milestone creation request")] CreateMilestoneRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateMilestoneAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Milestone successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "evidence_update_evidence", Title = "Update Evidence Metadata", Destructive = false)]
|
||||||
|
[Description("Updates verification status, review notes, or metadata for an evidence submission.")]
|
||||||
|
public async Task<McpToolResult> UpdateEvidenceAsync([Description("Evidence update request")] UpdateEvidenceRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateEvidenceAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Evidence metadata successfully updated.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Execution;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Execution Services (M06 Disbursement Control, M09 Risk Management, Gating Rules BR-003).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class ExecutionMcpTools(IExecutionService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "execution_get_disbursements", Title = "Get Disbursements", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of disbursement tranche and drawdown records (Module M06).")]
|
||||||
|
public async Task<Records<Disbursement[]>> GetDisbursementsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetDisbursementsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_get_disbursement", Title = "Get Disbursement by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a disbursement request by its hash identifier.")]
|
||||||
|
public async Task<Records<Disbursement>> GetDisbursementAsync([Description("Disbursement hash identifier")] string disbursementIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetDisbursementAsync(disbursementIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_create_disbursement", Title = "Create Disbursement Request", Destructive = false)]
|
||||||
|
[Description("Submits a drawdown / payment disbursement request linked to a satisfied milestone (BR-003, Module M06).")]
|
||||||
|
public async Task<McpToolResult> CreateDisbursementAsync([Description("Disbursement request details")] CreateDisbursementRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateDisbursementAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Disbursement request successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_approve_disbursement", Title = "Approve Disbursement", Destructive = false)]
|
||||||
|
[Description("Formally approves release of funds for a disbursement tranche after verifying milestone evidence and conditions precedent (BR-003, AT-03).")]
|
||||||
|
public async Task<McpToolResult> ApproveDisbursementAsync([Description("Disbursement hash identifier")] string disbursementIdHash, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.ApproveDisbursementAsync(disbursementIdHash, userId ?? SystemUser, cancellationToken)).ToMcpResult("Disbursement tranche successfully approved for release.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_update_disbursement", Title = "Update Disbursement", Destructive = false)]
|
||||||
|
[Description("Updates metadata, bank payment references, or status of a disbursement tranche.")]
|
||||||
|
public async Task<McpToolResult> UpdateDisbursementAsync([Description("Disbursement update request")] UpdateDisbursementRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateDisbursementAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Disbursement record successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_get_risks", Title = "Get Risk Register", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of risks identified across portfolio projects (Module M09).")]
|
||||||
|
public async Task<Records<Risk[]>> GetRisksAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetRisksAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_get_risk", Title = "Get Risk by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a project risk, including likelihood, impact, score, and mitigations.")]
|
||||||
|
public async Task<Records<Risk>> GetRiskAsync([Description("Risk hash identifier")] string riskIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetRiskAsync(riskIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_create_risk", Title = "Log Risk", Destructive = false)]
|
||||||
|
[Description("Logs a new risk event with likelihood, severity, residual risk, and mitigation action plan (Module M09).")]
|
||||||
|
public async Task<McpToolResult> CreateRiskAsync([Description("Risk log request")] CreateRiskRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateRiskAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Risk record successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_update_risk", Title = "Update Risk & Mitigation", Destructive = false)]
|
||||||
|
[Description("Updates a risk's assessment score, status, or mitigation progress.")]
|
||||||
|
public async Task<McpToolResult> UpdateRiskAsync([Description("Risk update parameters")] UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateRiskAsync(request, cancellationToken)).ToMcpResult("Risk record successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_get_rules", Title = "Get Gating Rules", ReadOnly = true)]
|
||||||
|
[Description("Retrieves configurable gating and compliance validation rules (Module M06 / BR-003).")]
|
||||||
|
public async Task<Records<Rule[]>> GetRulesAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetRulesAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_get_rule", Title = "Get Rule by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a gating validation rule by hash identifier.")]
|
||||||
|
public async Task<Records<Rule>> GetRuleAsync([Description("Rule hash identifier")] string ruleIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetRuleAsync(ruleIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_create_rule", Title = "Create Gating Rule", Destructive = false)]
|
||||||
|
[Description("Defines a new business or compliance gating rule governing disbursement release.")]
|
||||||
|
public async Task<McpToolResult> CreateRuleAsync([Description("Rule creation request")] CreateRuleRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateRuleAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Gating rule successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "execution_update_rule", Title = "Update Gating Rule", Destructive = false)]
|
||||||
|
[Description("Updates the logic or parameters of an existing gating rule.")]
|
||||||
|
public async Task<McpToolResult> UpdateRuleAsync([Description("Rule update request")] UpdateRuleRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateRuleAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Gating rule successfully updated.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Governance;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Governance Services (Portfolios, Organisations, Multi-Tenancy, Funding Instruments, Executive Dashboards).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class GovernanceMcpTools(IGovernanceService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "governance_get_dashboard", Title = "Get Portfolio Executive Dashboard", ReadOnly = true)]
|
||||||
|
[Description("Retrieves aggregated executive portfolio statistics, project counts, financial totals, and risk overviews (Modules M20 & M22).")]
|
||||||
|
public async Task<Records<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetDashboardAsync(cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_organisations", Title = "Get Master Organisations", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of recipient, partner, and donor organisations (BR-005 Single Source of Truth).")]
|
||||||
|
public async Task<Records<Organisation[]>> GetOrganisationsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetOrganisationsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_organisation", Title = "Get Organisation by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves full master data and KYC details for an organisation by hash identifier.")]
|
||||||
|
public async Task<Records<Organisation>> GetOrganisationAsync([Description("Organisation hash identifier")] string organisationIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetOrganisationAsync(organisationIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_create_organisation", Title = "Register Organisation", Destructive = false)]
|
||||||
|
[Description("Registers a new recipient organisation, donor, or implementation partner (Module M03).")]
|
||||||
|
public async Task<McpToolResult> CreateOrganisationAsync([Description("Organisation creation details")] CreateOrganisationRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateOrganisationAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Organisation successfully registered.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_update_organisation", Title = "Update Organisation", Destructive = false)]
|
||||||
|
[Description("Updates organisation profile, contact details, registration, or tax status.")]
|
||||||
|
public async Task<Records<Organisation>> UpdateOrganisationAsync([Description("Organisation update request")] UpdateOrganisationRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await service.UpdateOrganisationAsync(request, userId ?? SystemUser, cancellationToken);
|
||||||
|
|
||||||
|
return result.ToMcpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_portfolios", Title = "Get Portfolios", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of top-level investment and grant portfolios.")]
|
||||||
|
public async Task<Records<Portfolio[]>> GetPortfoliosAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetPorfoliosAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_portfolio", Title = "Get Portfolio by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a portfolio by hash identifier.")]
|
||||||
|
public async Task<Records<Portfolio>> GetPortfolioAsync([Description("Portfolio hash identifier")] string portfolioIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetPortfolioAsync(portfolioIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_create_portfolio", Title = "Create Portfolio", Destructive = false)]
|
||||||
|
[Description("Creates an overarching investment or grant portfolio governing programmes.")]
|
||||||
|
public async Task<McpToolResult> CreatePortfolioAsync([Description("Portfolio creation parameters")] CreatePortfolioRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreatePortfolioAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Portfolio successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_update_portfolio", Title = "Update Portfolio", Destructive = false)]
|
||||||
|
[Description("Updates portfolio objectives, dates, or metadata.")]
|
||||||
|
public async Task<McpToolResult> UpdatePortfolioAsync([Description("Portfolio update request")] UpdatePortfolioRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdatePortfolioAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Portfolio successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_instruments", Title = "Get Funding Instruments", ReadOnly = true)]
|
||||||
|
[Description("Retrieves configured funding instruments (Grant, Debt, Impact, Blended Finance, etc.) under a programme (BR-006).")]
|
||||||
|
public async Task<Records<Instrument[]>> GetInstrumentsAsync([Description("Programme hash identifier")] string programmeIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetInstrumentsAsync(programmeIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_create_instrument", Title = "Define Funding Instrument", Destructive = false)]
|
||||||
|
[Description("Configures a funding instrument with custom milestone rules, KPIs, and disbursement criteria (BR-006).")]
|
||||||
|
public async Task<McpToolResult> CreateInstrumentAsync([Description("Instrument creation request")] CreateInstrumentRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateInstrumentAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Funding instrument successfully defined.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_tenants", Title = "Get Tenants", ReadOnly = true)]
|
||||||
|
[Description("Retrieves registered tenant partitions in the multi-tenant architecture.")]
|
||||||
|
public async Task<Records<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetTenantsAsync(cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_tenant", Title = "Get Tenant by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves tenant profile and isolation partition details.")]
|
||||||
|
public async Task<Records<Tenant>> GetTenantAsync([Description("Tenant hash identifier")] string tenantIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetTenantAsync(tenantIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_create_tenant", Title = "Create Tenant", Destructive = false)]
|
||||||
|
[Description("Provisions a new tenant boundary in the platform.")]
|
||||||
|
public async Task<McpToolResult> CreateTenantAsync([Description("Tenant creation parameters")] CreateTenantRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateTenantAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Tenant successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_portfolio_value", Title = "Calculate Portfolio Monetary Value", ReadOnly = true)]
|
||||||
|
[Description("Computes aggregated total commitment and monetary allocation across all projects in a portfolio (BR-010).")]
|
||||||
|
public async Task<Records<decimal>> GetPortfolioValueAsync([Description("Portfolio hash identifier")] string portfolioIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetPorfolioValueAsync(portfolioIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "governance_get_programme_value", Title = "Calculate Programme Monetary Value", ReadOnly = true)]
|
||||||
|
[Description("Computes aggregated total commitment across all projects in a programme (BR-010).")]
|
||||||
|
public async Task<Records<decimal>> GetProgrammeValueAsync([Description("Programme hash identifier")] string programmeIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProgrammeValueAsync(programmeIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Grants & Award Services (M01 Award Intake, M02 Contract Management, M05 Budgeting).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class GrantsMcpTools(IGrantsService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "grants_get_awards", Title = "Get Approved Funding Awards", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of approved funding awards (Module M01).")]
|
||||||
|
public async Task<Records<Award[]>> GetAwardsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetAwardsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_award", Title = "Get Award by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of an approved funding award by its hash identifier.")]
|
||||||
|
public async Task<Records<Award>> GetAwardAsync([Description("Award hash identifier")] string awardIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetAwardAsync(awardIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_create_award", Title = "Register Approved Award", Destructive = false)]
|
||||||
|
[Description("Registers an approved funding award from upstream committee sanction (BR-001/BR-002, M01). Automatically transitions into contract generation.")]
|
||||||
|
public async Task<McpToolResult> CreateAwardAsync([Description("Award creation details")] CreateAwardRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateAwardAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Award record successfully created and transitioned to contract generation.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_update_award", Title = "Update Award Record", Destructive = false)]
|
||||||
|
[Description("Updates an existing funding award's metadata and parameters.")]
|
||||||
|
public async Task<McpToolResult> UpdateAwardAsync([Description("Award update parameters")] UpdateAwardRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateAwardAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Award record successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_contracts", Title = "Get Contracts", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of legal grant contracts and agreements (Module M02).")]
|
||||||
|
public async Task<Records<Contract[]>> GetContractsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetContractsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_contract", Title = "Get Contract by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a contract agreement by its hash identifier.")]
|
||||||
|
public async Task<Records<Contract>> GetContractAsync([Description("Contract hash identifier")] string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetContractAsync(contractIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_create_contract", Title = "Create Draft Contract", Destructive = false)]
|
||||||
|
[Description("Creates a new draft contract or agreement associated with an approved award (Module M02).")]
|
||||||
|
public async Task<McpToolResult> CreateContractAsync([Description("Contract creation request")] CreateContractRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateContractAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Draft contract agreement successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_update_contract", Title = "Update Contract Agreement", Destructive = false)]
|
||||||
|
[Description("Updates terms and metadata of a contract in DRAFT or UNDER_REVIEW state.")]
|
||||||
|
public async Task<McpToolResult> UpdateContractAsync([Description("Contract update request")] UpdateContractRequest request, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateContractAsync(request, cancellationToken)).ToMcpResult("Contract agreement successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_execute_contract", Title = "Formally Execute Contract", Destructive = false)]
|
||||||
|
[Description("Formally executes and locks a contract agreement once all parties have signed and effective date is established (FR-M02-020).")]
|
||||||
|
public async Task<McpToolResult> ExecuteContractAsync([Description("Contract execution request")] ExecuteContractRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.ExecuteContractAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Contract agreement successfully executed and locked.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_contract_url", Title = "Get Contract Download URL", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a presigned download URL for the authoritative executed contract document.")]
|
||||||
|
public async Task<Records<string>> GetContractUrlAsync([Description("Contract hash identifier")] string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetContractUrlAsync(contractIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_budgets", Title = "Get Budgets", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of project budgets and allocations (Module M05).")]
|
||||||
|
public async Task<Records<Budget[]>> GetBudgetsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetBudgetsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_get_budget", Title = "Get Budget by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a project budget allocation by hash identifier.")]
|
||||||
|
public async Task<Records<Budget>> GetBudgetAsync([Description("Budget hash identifier")] string budgetIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetBudgetAsync(budgetIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_create_budget", Title = "Create Budget Allocation", Destructive = false)]
|
||||||
|
[Description("Creates a new project budget line and category allocation (Module M05).")]
|
||||||
|
public async Task<McpToolResult> CreateBudgetAsync([Description("Budget creation request")] CreateBudgetRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateBudgetAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Budget allocation successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "grants_update_budget", Title = "Update Budget Allocation", Destructive = false)]
|
||||||
|
[Description("Updates an existing project budget allocation with audit tracking.")]
|
||||||
|
public async Task<McpToolResult> UpdateBudgetAsync([Description("Budget update request")] UpdateBudgetRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateBudgetAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Budget allocation successfully updated.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Mcp;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
public class InAppMcpClient : IInAppMcpClient
|
||||||
|
{
|
||||||
|
private static bool initialized;
|
||||||
|
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
private readonly ILoggerFactory loggerFactory;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<string, ToolMethodInfo> ToolRegistry = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly List<McpToolDefinition> ToolDefinitions = [];
|
||||||
|
private static readonly Lock InitLock = new();
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Type[] ToolTypes =
|
||||||
|
[
|
||||||
|
typeof(GrantsMcpTools),
|
||||||
|
typeof(ProjectsMcpTools),
|
||||||
|
typeof(ExecutionMcpTools),
|
||||||
|
typeof(GovernanceMcpTools),
|
||||||
|
typeof(EvidenceMcpTools),
|
||||||
|
typeof(CommunicationsMcpTools),
|
||||||
|
typeof(UsersMcpTools)
|
||||||
|
];
|
||||||
|
|
||||||
|
public InAppMcpClient(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
this.loggerFactory = loggerFactory;
|
||||||
|
|
||||||
|
EnsureInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (initialized) return;
|
||||||
|
|
||||||
|
lock (InitLock)
|
||||||
|
{
|
||||||
|
if (initialized) return;
|
||||||
|
|
||||||
|
foreach (var toolType in ToolTypes)
|
||||||
|
{
|
||||||
|
var groupName = toolType.Name.Replace("McpTools", string.Empty);
|
||||||
|
|
||||||
|
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
var toolAttr = method.GetCustomAttribute<McpServerToolAttribute>();
|
||||||
|
|
||||||
|
if (toolAttr == null) continue;
|
||||||
|
|
||||||
|
var toolName = !string.IsNullOrWhiteSpace(toolAttr.Name) ? toolAttr.Name : method.Name;
|
||||||
|
var title = !string.IsNullOrWhiteSpace(toolAttr.Title) ? toolAttr.Title : toolName;
|
||||||
|
var descAttr = method.GetCustomAttribute<DescriptionAttribute>();
|
||||||
|
var description = descAttr?.Description ?? title;
|
||||||
|
|
||||||
|
var parameters = new List<McpToolParameter>();
|
||||||
|
var methodParams = method.GetParameters();
|
||||||
|
|
||||||
|
foreach (var p in methodParams)
|
||||||
|
{
|
||||||
|
if (p.ParameterType == typeof(CancellationToken)) continue;
|
||||||
|
|
||||||
|
var pDescAttr = p.GetCustomAttribute<DescriptionAttribute>();
|
||||||
|
|
||||||
|
parameters.Add(new McpToolParameter(Name: p.Name ?? "param", Type: p.ParameterType.Name, Description: pDescAttr?.Description ?? string.Empty,
|
||||||
|
IsRequired: !p.HasDefaultValue && !IsNullable(p.ParameterType), DefaultValue: p.HasDefaultValue ? p.DefaultValue : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
var toolDef = new McpToolDefinition(Name: toolName, Title: title, Description: description, Group: groupName,
|
||||||
|
ReadOnly: toolAttr.ReadOnly, Parameters: parameters);
|
||||||
|
|
||||||
|
ToolDefinitions.Add(toolDef);
|
||||||
|
|
||||||
|
ToolRegistry[toolName] = new ToolMethodInfo(toolType, method, methodParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<McpToolDefinition> GetAvailableTools() => ToolDefinitions;
|
||||||
|
|
||||||
|
public async Task<McpToolExecutionResult> ExecuteToolAsync(string toolName, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!ToolRegistry.TryGetValue(toolName, out var toolMethod))
|
||||||
|
return McpToolExecutionResult.Fail($"MCP tool '{toolName}' is not recognized.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var targetInstance = serviceProvider.GetRequiredService(toolMethod.TargetType);
|
||||||
|
var args = BindArguments(toolMethod.Parameters, arguments, cancellationToken);
|
||||||
|
|
||||||
|
var invokeResult = toolMethod.Method.Invoke(targetInstance, args);
|
||||||
|
|
||||||
|
if (invokeResult is Task task)
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result");
|
||||||
|
var value = resultProperty?.GetValue(task);
|
||||||
|
|
||||||
|
return McpToolExecutionResult.Ok(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return McpToolExecutionResult.Ok(invokeResult);
|
||||||
|
}
|
||||||
|
catch (TargetInvocationException ex)
|
||||||
|
{
|
||||||
|
var inner = ex.InnerException ?? ex;
|
||||||
|
|
||||||
|
return McpToolExecutionResult.Fail($"Execution of tool '{toolName}' failed: {inner.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return McpToolExecutionResult.Fail($"Execution of tool '{toolName}' failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpClient> CreateProtocolClientAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var serverToClient = new Pipe();
|
||||||
|
var clientToServer = new Pipe();
|
||||||
|
|
||||||
|
var serverIn = clientToServer.Reader.AsStream();
|
||||||
|
var serverOut = serverToClient.Writer.AsStream();
|
||||||
|
|
||||||
|
var clientIn = serverToClient.Reader.AsStream();
|
||||||
|
var clientOut = clientToServer.Writer.AsStream();
|
||||||
|
|
||||||
|
var inMemoryServices = new ServiceCollection();
|
||||||
|
inMemoryServices.AddLogging();
|
||||||
|
|
||||||
|
foreach (var t in ToolTypes)
|
||||||
|
inMemoryServices.AddScoped(t, _ => serviceProvider.GetRequiredService(t));
|
||||||
|
|
||||||
|
var mcpBuilder = inMemoryServices.AddMcpServer();
|
||||||
|
|
||||||
|
foreach (var t in ToolTypes)
|
||||||
|
mcpBuilder.WithTools(t, (JsonSerializerOptions?)null);
|
||||||
|
|
||||||
|
mcpBuilder.WithStreamServerTransport(serverIn, serverOut);
|
||||||
|
|
||||||
|
var inMemoryProvider = inMemoryServices.BuildServiceProvider();
|
||||||
|
var hostedServices = inMemoryProvider.GetServices<IHostedService>();
|
||||||
|
|
||||||
|
foreach (var hs in hostedServices)
|
||||||
|
_ = hs.StartAsync(cancellationToken);
|
||||||
|
|
||||||
|
var clientTransport = new StreamClientTransport(serverInput: clientOut, serverOutput: clientIn, loggerFactory);
|
||||||
|
|
||||||
|
return await McpClient.CreateAsync(clientTransport, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object?[] BindArguments(ParameterInfo[] parameters, IReadOnlyDictionary<string, object?>? arguments, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new object?[parameters.Length];
|
||||||
|
var args = arguments ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Length; i++)
|
||||||
|
{
|
||||||
|
var p = parameters[i];
|
||||||
|
|
||||||
|
if (p.ParameterType == typeof(CancellationToken))
|
||||||
|
{
|
||||||
|
result[i] = cancellationToken;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchKey = args.Keys.FirstOrDefault(k => string.Equals(k, p.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (matchKey != null && args.TryGetValue(matchKey, out var val) && val != null)
|
||||||
|
result[i] = ConvertValue(val, p.ParameterType);
|
||||||
|
else if (p.HasDefaultValue)
|
||||||
|
result[i] = p.DefaultValue;
|
||||||
|
else if (p.ParameterType == typeof(Guid) && p.Name?.Contains("userId", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
|
result[i] = SystemUser;
|
||||||
|
else if (IsNullable(p.ParameterType))
|
||||||
|
result[i] = null;
|
||||||
|
else
|
||||||
|
result[i] = p.ParameterType.IsValueType ? Activator.CreateInstance(p.ParameterType) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ConvertValue(object val, Type targetType)
|
||||||
|
{
|
||||||
|
if (val == null) return null;
|
||||||
|
|
||||||
|
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||||
|
|
||||||
|
if (underlying.IsInstanceOfType(val)) return val;
|
||||||
|
|
||||||
|
if (val is JsonElement jsonElement)
|
||||||
|
return JsonSerializer.Deserialize(jsonElement.GetRawText(), targetType, JsonOptions);
|
||||||
|
|
||||||
|
var str = val.ToString();
|
||||||
|
|
||||||
|
if (str == null) return null;
|
||||||
|
|
||||||
|
if (underlying == typeof(Guid)) return Guid.Parse(str);
|
||||||
|
|
||||||
|
if (underlying.IsEnum) return Enum.Parse(underlying, str, true);
|
||||||
|
|
||||||
|
if (underlying.IsPrimitive || underlying == typeof(decimal) || underlying == typeof(string) || underlying == typeof(DateTime))
|
||||||
|
return Convert.ChangeType(val, underlying, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize(JsonSerializer.Serialize(val), targetType, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNullable(Type type) =>
|
||||||
|
!type.IsValueType || Nullable.GetUnderlyingType(type) != null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Projects;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Project & Programme Management Services (M04 WBS/Activities, M07 Milestones, M10 Compliance, M13 Beneficiaries, M15 Change Control, M16 Issues, M17 Site Visits).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class ProjectsMcpTools(IProjectService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "projects_get_projects", Title = "Get Projects", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of funded projects (Module M04).")]
|
||||||
|
public async Task<Records<Project[]>> GetProjectsAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectsAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_project", Title = "Get Project by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves comprehensive details of a project by its hash identifier.")]
|
||||||
|
public async Task<Records<Project>> GetProjectAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_project", Title = "Create Project", Destructive = false)]
|
||||||
|
[Description("Creates and initializes a funded project under an active award/programme (Module M04).")]
|
||||||
|
public async Task<McpToolResult> CreateProjectAsync([Description("Project creation request")] CreateProjectRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateProjectAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Project successfully created.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_update_project", Title = "Update Project", Destructive = false)]
|
||||||
|
[Description("Updates project baseline configuration, scope, and objectives.")]
|
||||||
|
public async Task<McpToolResult> UpdateProjectAsync([Description("Project update request")] UpdateProjectRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateProjectAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Project details successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_update_project_status", Title = "Update Project Status", Destructive = false)]
|
||||||
|
[Description("Transitions a project's lifecycle status (e.g. Inception, Active, Under Review, Closeout, Completed).")]
|
||||||
|
public async Task<McpToolResult> UpdateProjectStatusAsync([Description("Status update request")] UpdateProjectStatusRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateProjectStatusAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult("Project status successfully updated.");
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_programmes", Title = "Get Funding Programmes", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of overarching funding programmes.")]
|
||||||
|
public async Task<Records<Programme[]>> GetProgrammesAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProgrammesAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_programme", Title = "Get Programme by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a funding programme by its hash identifier.")]
|
||||||
|
public async Task<Records<Programme>> GetProgrammeAsync([Description("Programme hash identifier")] string programmeIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProgrammeAsync(programmeIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_programme", Title = "Create Programme", Destructive = false)]
|
||||||
|
[Description("Creates a new funding programme governing awards and portfolios.")]
|
||||||
|
public async Task<McpToolResult> CreateProgrammeAsync([Description("Programme creation parameters")] CreateProgrammeRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateProgrammeAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_update_programme", Title = "Update Programme", Destructive = false)]
|
||||||
|
[Description("Updates details of an existing funding programme.")]
|
||||||
|
public async Task<McpToolResult> UpdateProgrammeAsync([Description("Programme update parameters")] UpdateProgrammeRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateProgrammeAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_programme_projects", Title = "Get Projects by Programme", ReadOnly = true)]
|
||||||
|
[Description("Retrieves all projects associated with a given funding programme.")]
|
||||||
|
public async Task<Records<Project[]>> GetProgrammeProjectsAsync([Description("Programme hash identifier")] string programmeIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_activities", Title = "Get Project Activities", ReadOnly = true)]
|
||||||
|
[Description("Retrieves the work breakdown structure (WBS) activities for a project.")]
|
||||||
|
public async Task<Records<Domain.Models.Activity[]>> GetActivitiesAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetActivitiesAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_activity", Title = "Get Activity by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a project activity by its hash identifier.")]
|
||||||
|
public async Task<Records<Domain.Models.Activity>> GetActivityAsync([Description("Activity hash identifier")] string activityIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetActivityAsync(activityIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_activity", Title = "Create Project Activity", Destructive = false)]
|
||||||
|
[Description("Creates a new work breakdown structure (WBS) activity under a project.")]
|
||||||
|
public async Task<McpToolResult> CreateActivityAsync([Description("Activity creation request")] CreateActivityRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateActivityAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_update_activity", Title = "Update Project Activity", Destructive = false)]
|
||||||
|
[Description("Updates progress, timeline, and status of a project activity.")]
|
||||||
|
public async Task<McpToolResult> UpdateActivityAsync([Description("Activity update request")] UpdateActivityRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateActivityAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_project_milestones", Title = "Get Project Milestones", ReadOnly = true)]
|
||||||
|
[Description("Retrieves all delivery and disbursement gating milestones for a project (Module M07).")]
|
||||||
|
public async Task<Records<Milestone[]>> GetProjectMilestonesAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_milestones", Title = "Get All Milestones", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated list of milestones across the portfolio.")]
|
||||||
|
public async Task<Records<Milestone[]>> GetMilestonesAsync([Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetMilestonesAsync(new Pagination(page, pageSize), cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_milestone", Title = "Get Milestone by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves details of a project milestone by its hash identifier.")]
|
||||||
|
public async Task<Records<Milestone>> GetMilestoneAsync([Description("Milestone hash identifier")] string milestoneIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetMilestoneAsync(milestoneIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_update_milestone", Title = "Update Milestone", Destructive = false)]
|
||||||
|
[Description("Updates milestone progress, delivery dates, verification status, and completion state.")]
|
||||||
|
public async Task<McpToolResult> UpdateMilestoneAsync([Description("Milestone update request")] UpdateMilestoneRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateMilestoneAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_project_risks", Title = "Get Project Risks", ReadOnly = true)]
|
||||||
|
[Description("Retrieves risk register items specifically identified for a project.")]
|
||||||
|
public async Task<Records<Risk[]>> GetProjectRisksAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectRisksAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_project_indicators", Title = "Get Project Indicators", ReadOnly = true)]
|
||||||
|
[Description("Retrieves M&E performance indicators configured for a project (Module M08).")]
|
||||||
|
public async Task<Records<Indicator[]>> GetProjectIndicatorsAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_beneficiaries", Title = "Get Beneficiaries", ReadOnly = true)]
|
||||||
|
[Description("Retrieves POPIA-compliant beneficiary demographic records for a project (Module M13).")]
|
||||||
|
public async Task<Records<Beneficiary[]>> GetBeneficiariesAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetBeneficiariesAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_beneficiary", Title = "Register Beneficiary Group", Destructive = false)]
|
||||||
|
[Description("Registers reach and demographic beneficiary data for a project (Module M13).")]
|
||||||
|
public async Task<McpToolResult> CreateBeneficiaryAsync([Description("Beneficiary registration request")] CreateBeneficiaryRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateBeneficiaryAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_change_requests", Title = "Get Change Requests", ReadOnly = true)]
|
||||||
|
[Description("Retrieves project scope, timeline, and budget change requests (Module M15).")]
|
||||||
|
public async Task<Records<ChangeRequest[]>> GetChangeRequestsAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetChangeRequestsAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_change_request", Title = "Submit Change Request", Destructive = false)]
|
||||||
|
[Description("Submits a formal change request for project scope, budget, or timeline amendment.")]
|
||||||
|
public async Task<McpToolResult> CreateChangeRequestAsync([Description("Change request details")] CreateChangeRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateChangeRequestAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_compliance_items", Title = "Get Compliance Items", ReadOnly = true)]
|
||||||
|
[Description("Retrieves statutory, tax, and ESG compliance checklist items for a project (Module M10).")]
|
||||||
|
public async Task<Records<ComplianceItem[]>> GetComplianceItemsAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetComplianceItemsAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_compliance_item", Title = "Create Compliance Item", Destructive = false)]
|
||||||
|
[Description("Creates a compliance or statutory requirement item for monitoring.")]
|
||||||
|
public async Task<McpToolResult> CreateComplianceItemAsync([Description("Compliance item details")] CreateComplianceItemRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateComplianceItemAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_issues", Title = "Get Project Issues", ReadOnly = true)]
|
||||||
|
[Description("Retrieves operational issue log items and blockers for a project (Module M16).")]
|
||||||
|
public async Task<Records<Issue[]>> GetIssuesAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetIssuesAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_issue", Title = "Log Issue", Destructive = false)]
|
||||||
|
[Description("Logs an operational issue or bottleneck against a project for management tracking.")]
|
||||||
|
public async Task<McpToolResult> CreateIssueAsync([Description("Issue log request")] CreateIssueRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateIssueAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_get_site_visits", Title = "Get Site Visits", ReadOnly = true)]
|
||||||
|
[Description("Retrieves scheduled and completed physical or virtual monitoring site visits (Module M17).")]
|
||||||
|
public async Task<Records<SiteVisit[]>> GetSiteVisitsAsync([Description("Project hash identifier")] string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetSiteVisitsAsync(projectIdHash, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "projects_create_site_visit", Title = "Schedule Site Visit", Destructive = false)]
|
||||||
|
[Description("Schedules an in-person or remote monitoring site inspection for milestone verification.")]
|
||||||
|
public async Task<McpToolResult> CreateSiteVisitAsync([Description("Site visit schedule request")] CreateSiteVisitRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateSiteVisitAsync(request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token-optimized standardized tool response wrapper for MCP tool execution.
|
||||||
|
/// </summary>
|
||||||
|
public record Records<T>(bool Success, T? Data = default, string? Message = null, IReadOnlyList<string>? Errors = null)
|
||||||
|
{
|
||||||
|
public static Records<T> Ok(T data, string? message = null) => new(true, data, message, null);
|
||||||
|
|
||||||
|
public static Records<T> Fail(string error, IReadOnlyList<string>? errors = null) => new(false, default, error, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token-optimized standardized non-generic tool response wrapper for MCP tool execution.
|
||||||
|
/// </summary>
|
||||||
|
public record McpToolResult(bool Success, string Message, IReadOnlyList<string>? Errors = null)
|
||||||
|
{
|
||||||
|
public static McpToolResult Ok(string message = "Operation completed successfully.") => new(true, message);
|
||||||
|
|
||||||
|
public static McpToolResult Fail(string error, IReadOnlyList<string>? errors = null) => new(false, error, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record McpToolDefinition(string Name, string Title, string Description, string Group, bool ReadOnly, IReadOnlyList<McpToolParameter> Parameters);
|
||||||
|
|
||||||
|
public sealed record McpToolParameter(string Name, string Type, string Description, bool IsRequired, object? DefaultValue);
|
||||||
|
|
||||||
|
public sealed record McpToolExecutionResult(bool Success, object? Data, string? Error, string? SerializedResult)
|
||||||
|
{
|
||||||
|
public static McpToolExecutionResult Ok(object? data, JsonSerializerOptions? options = null) =>
|
||||||
|
new(true, data, null, data != null ? JsonSerializer.Serialize(data, options ?? DefaultJsonOptions) : null);
|
||||||
|
|
||||||
|
public static McpToolExecutionResult Fail(string error) =>
|
||||||
|
new(false, null, error, null);
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions DefaultJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ToolMethodInfo(Type TargetType, MethodInfo Method, ParameterInfo[] Parameters);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Services.Users;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Models.Security;
|
||||||
|
using Role = PostFundManagement.Domain.Models.Security.Role;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools proxying Identity & User Administration Services (RBAC Roles, Claims, Account Provisioning).
|
||||||
|
/// </summary>
|
||||||
|
[McpServerToolType]
|
||||||
|
public class UsersMcpTools(IUserService service)
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "users_get_users", Title = "Get User Directory", ReadOnly = true)]
|
||||||
|
[Description("Retrieves a paginated and filtered list of user accounts across tenant partitions.")]
|
||||||
|
public async Task<Records<User[]>> GetUsersAsync([Description("User search and filtering parameters")] UserFilterRequest filter, [Description("Page index (1-based)")] int page = 1, [Description("Page size (1-100)")] int pageSize = 50, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetUsersAsync(new Pagination(page, pageSize), filter, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_get_user", Title = "Get User by ID", ReadOnly = true)]
|
||||||
|
[Description("Retrieves account profile and security claims for a specific user.")]
|
||||||
|
public async Task<Records<User>> GetUserAsync([Description("Target user identifier")] Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetUserAsync(userId, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_create_user", Title = "Create User Account", Destructive = false)]
|
||||||
|
[Description("Provisions a new user account within a tenant organization.")]
|
||||||
|
public async Task<McpToolResult> CreateUserAsync([Description("Tenant hash identifier")] string tenantIdHash, [Description("Hashed ID number or passport")] string idNumberHash, [Description("User account creation parameters")] CreateUserRequest request, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.CreateUserAsync(tenantIdHash, idNumberHash, request, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_update_status", Title = "Update User Account Status", Destructive = false)]
|
||||||
|
[Description("Updates active, suspended, or archived state of a user account.")]
|
||||||
|
public async Task<McpToolResult> UpdateUserStatusAsync([Description("Target user identifier")] Guid userId, [Description("New activity status")] ActivityStatus status, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.UpdateUserStatusAsync(userId, status, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_get_system_roles", Title = "Get System Roles", ReadOnly = true)]
|
||||||
|
[Description("Retrieves system-wide security and governance roles.")]
|
||||||
|
public async Task<Records<Role[]>> GetSystemRolesAsync([Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetSystemRolesAsync(userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_get_user_roles", Title = "Get User Roles", ReadOnly = true)]
|
||||||
|
[Description("Retrieves assigned RBAC roles for a specific user.")]
|
||||||
|
public async Task<Records<Role[]>> GetUserRolesAsync([Description("Target user identifier")] Guid targetUserId, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetUserRolesAsync(targetUserId, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_add_role", Title = "Assign Role to User", Destructive = false)]
|
||||||
|
[Description("Assigns a security role to a user account.")]
|
||||||
|
public async Task<McpToolResult> AddUserRoleAsync([Description("Target user identifier")] Guid targetUserId, [Description("Role identifier to assign")] Guid roleId, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.AddUserRoleAsync(targetUserId, roleId, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_delete_role", Title = "Revoke Role from User", Destructive = false)]
|
||||||
|
[Description("Revokes an assigned security role from a user account.")]
|
||||||
|
public async Task<McpToolResult> DeleteUserRoleAsync([Description("Target user identifier")] Guid targetUserId, [Description("Role identifier to revoke")] Guid roleId, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.DeleteUserRoleAsync(targetUserId, roleId, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_get_user_claims", Title = "Get User Claims", ReadOnly = true)]
|
||||||
|
[Description("Retrieves specific identity and authorization claims associated with a user.")]
|
||||||
|
public async Task<Records<Domain.Models.Security.Claim[]>> GetUserClaimsAsync([Description("Target user identifier")] Guid targetUserId, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.GetUserClaimsAsync(targetUserId, userId ?? SystemUser, cancellationToken)).ToMcpResult();
|
||||||
|
|
||||||
|
[McpServerTool(Name = "users_set_lockout", Title = "Set User Lockout", Destructive = false)]
|
||||||
|
[Description("Locks or unlocks a user account from authenticating into the platform.")]
|
||||||
|
public async Task<McpToolResult> SetUserLockoutAsync([Description("Target user identifier")] Guid targetUserId, [Description("True to lockout/disable account, false to re-enable")] bool disable, [Description("Optional acting user ID; defaults to System user if omitted")] Guid? userId = null, CancellationToken cancellationToken = default) =>
|
||||||
|
(await service.SetUserLockoutAsync(targetUserId, userId ?? SystemUser, disable, cancellationToken)).ToMcpResult();
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="Duende.AccessTokenManagement.OpenIdConnect" />
|
<PackageReference Include="Duende.AccessTokenManagement.OpenIdConnect" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Validation" />
|
<PackageReference Include="Microsoft.Extensions.Validation" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" />
|
<PackageReference Include="Scalar.AspNetCore" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Communications;
|
namespace PostFundManagement.Application.Services.Communications;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Communications;
|
namespace PostFundManagement.Application.Services.Communications;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Evidence;
|
namespace PostFundManagement.Application.Services.Evidence;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Evidence;
|
namespace PostFundManagement.Application.Services.Evidence;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Execution;
|
namespace PostFundManagement.Application.Services.Execution;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Execution;
|
namespace PostFundManagement.Application.Services.Execution;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Governance;
|
namespace PostFundManagement.Application.Services.Governance;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Governance;
|
namespace PostFundManagement.Application.Services.Governance;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Grants;
|
namespace PostFundManagement.Application.Services.Grants;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Grants;
|
namespace PostFundManagement.Application.Services.Grants;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
|
|||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Projects;
|
namespace PostFundManagement.Application.Services.Projects;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Projects;
|
namespace PostFundManagement.Application.Services.Projects;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using PostFundManagement.Domain.Errors;
|
|||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ public sealed class AuditService(IDbContextFactory<ApplicationDbContext> context
|
|||||||
if (!tenantContextService.IsAdmin)
|
if (!tenantContextService.IsAdmin)
|
||||||
{
|
{
|
||||||
var currentTenantId = tenantContextService.TenantId > 0 ? tenantContextService.TenantId : 1L;
|
var currentTenantId = tenantContextService.TenantId > 0 ? tenantContextService.TenantId : 1L;
|
||||||
|
|
||||||
query = query.Where(o => o.TenantId == currentTenantId);
|
query = query.Where(o => o.TenantId == currentTenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,15 +36,7 @@ public sealed class AuditService(IDbContextFactory<ApplicationDbContext> context
|
|||||||
return Result.Ok(logs.Select(p => p.Map()).ToArray());
|
return Result.Ok(logs.Select(p => p.Map()).ToArray());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
public async Task<Result> LogActionAsync(
|
public async Task<Result> LogActionAsync(string entityId, string entityName, string action, string[] changes, Guid performedBy, string? error = null, long? tenantId = null, CancellationToken cancellationToken = default) =>
|
||||||
string entityId,
|
|
||||||
string entityName,
|
|
||||||
string action,
|
|
||||||
string[] changes,
|
|
||||||
Guid performedBy,
|
|
||||||
string? error = null,
|
|
||||||
long? tenantId = null,
|
|
||||||
CancellationToken cancellationToken = default) =>
|
|
||||||
await Result.Try((Func<Task<Result>>)(async () =>
|
await Result.Try((Func<Task<Result>>)(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Domain.Errors;
|
using PostFundManagement.Domain.Errors;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain;
|
|||||||
using PostFundManagement.Domain.Configuration.Email;
|
using PostFundManagement.Domain.Configuration.Email;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models.Email;
|
using PostFundManagement.Domain.Models.Email;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Errors;
|
using PostFundManagement.Domain.Errors;
|
||||||
using PostFundManagement.Application.Extensions;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Application.Abstractions.Services;
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Errors;
|
using PostFundManagement.Domain.Errors;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Configuration.Security;
|
|||||||
using PostFundManagement.Domain.Errors;
|
using PostFundManagement.Domain.Errors;
|
||||||
using PostFundManagement.Domain.Models.Security;
|
using PostFundManagement.Domain.Models.Security;
|
||||||
using PostFundManagement.Infrastructure.Security;
|
using PostFundManagement.Infrastructure.Security;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Shared;
|
namespace PostFundManagement.Application.Services.Shared;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using PostFundManagement.Domain;
|
|||||||
using PostFundManagement.Domain.Models;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Models.Security;
|
using PostFundManagement.Domain.Models.Security;
|
||||||
using Claim = PostFundManagement.Domain.Models.Security.Claim;
|
using Claim = PostFundManagement.Domain.Models.Security.Claim;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
using Role = PostFundManagement.Domain.Models.Security.Role;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Users;
|
namespace PostFundManagement.Application.Services.Users;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using PostFundManagement.Domain.Models;
|
|||||||
using PostFundManagement.Domain.Models.Security;
|
using PostFundManagement.Domain.Models.Security;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
using Claim = PostFundManagement.Domain.Models.Security.Claim;
|
using Claim = PostFundManagement.Domain.Models.Security.Claim;
|
||||||
|
using Result = FluentResults.Result;
|
||||||
|
using Role = PostFundManagement.Domain.Models.Security.Role;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Services.Users;
|
namespace PostFundManagement.Application.Services.Users;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Stage 1: Runtime base
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
|
USER app
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Stage 2: Build & Restore
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy props and csproj files for layer caching
|
||||||
|
COPY ["Directory.Build.props", "./"]
|
||||||
|
COPY ["Directory.Packages.props", "./"]
|
||||||
|
COPY ["PostFundManagement.snk", "./"]
|
||||||
|
COPY ["LICENSE", "./"]
|
||||||
|
COPY ["PostFundManagement.Mcp/PostFundManagement.Mcp.csproj", "PostFundManagement.Mcp/"]
|
||||||
|
COPY ["PostFundManagement.Application/PostFundManagement.Application.csproj", "PostFundManagement.Application/"]
|
||||||
|
COPY ["PostFundManagement.Domain/PostFundManagement.Domain.csproj", "PostFundManagement.Domain/"]
|
||||||
|
COPY ["PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj", "PostFundManagement.Infrastructure/"]
|
||||||
|
|
||||||
|
# Restore packages
|
||||||
|
RUN dotnet restore "PostFundManagement.Mcp/PostFundManagement.Mcp.csproj"
|
||||||
|
|
||||||
|
# Copy full source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build from the project folder using relative filename
|
||||||
|
WORKDIR "/src/PostFundManagement.Mcp"
|
||||||
|
RUN dotnet build "PostFundManagement.Mcp.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
# Stage 3: Publish
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "PostFundManagement.Mcp.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
# Stage 4: Final Runtime
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "PostFundManagement.Mcp.dll"]
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<UserSecretsId>dotnet-PostFundManagement.Mcp-38a42b10-67c2-4889-b1d5-89f5cda11234</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Health Checks -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Model Context Protocol -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ModelContextProtocol.AspNetCore" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- File Nesting -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectCapability Include="ConfigurableFileNesting" />
|
||||||
|
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- CQRS Source Generator -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Mediator.SourceGenerator">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PostFundManagement.Application\PostFundManagement.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Health;
|
||||||
|
using PostFundManagement.Domain.Mediator;
|
||||||
|
using PostFundManagement.Infrastructure.Database.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddFeatureManagement();
|
||||||
|
builder.Services.AddSharedServices();
|
||||||
|
builder.Services.AddRegisterServicesWithCaching(builder.Configuration);
|
||||||
|
builder.Services.AddEndpointAndServiceCaching(builder.Configuration);
|
||||||
|
builder.Services.AddTransient<ITenantContextService, HeadlessTenantContextService>();
|
||||||
|
builder.Services.AddSkorubaApiService(builder.Configuration);
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
builder.Services.AddMediator();
|
||||||
|
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
|
||||||
|
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
|
||||||
|
|
||||||
|
builder.Services.AddEmailServices(builder.Configuration);
|
||||||
|
builder.Services.AddHashServices(builder.Configuration);
|
||||||
|
builder.Services.AddGarageS3(builder.Configuration);
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddDataProtectionDatabase(builder.Configuration);
|
||||||
|
builder.Services.AddApplicationDbContext(builder.Configuration);
|
||||||
|
|
||||||
|
// Register application MCP tooling
|
||||||
|
builder.Services.AddApplicationMcpTools();
|
||||||
|
|
||||||
|
var isStdioMode = args.Contains("--stdio", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isStdioMode)
|
||||||
|
{
|
||||||
|
// Local CLI / Stdio transport mode (e.g. for Claude Desktop, local cursor, or command-line agents)
|
||||||
|
builder.Services.AddMcpServer()
|
||||||
|
.WithApplicationMcpTools()
|
||||||
|
.WithStdioServerTransport();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Web / SSE HTTP transport mode (for network agents, external LLM platforms, and remote orchestrators)
|
||||||
|
builder.Services.AddMcpServer()
|
||||||
|
.WithApplicationMcpTools()
|
||||||
|
.WithHttpTransport();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddHealthChecks()
|
||||||
|
.AddCheck<PostgresHealthCheck>("PostgresDatabase")
|
||||||
|
.AddCheck("Self", () => HealthCheckResult.Healthy());
|
||||||
|
|
||||||
|
var allowedOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[]>() ?? [];
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(CorsPolicy, policy =>
|
||||||
|
{
|
||||||
|
policy.SetIsOriginAllowed(origin =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(origin)) return false;
|
||||||
|
|
||||||
|
var clean = origin.TrimEnd('/');
|
||||||
|
if (allowedOrigins.Any(a => string.Equals(a.TrimEnd('/'), clean, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var host = new Uri(origin).Host;
|
||||||
|
if (host.EndsWith("khongisa.co.za", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
if (!isStdioMode)
|
||||||
|
{
|
||||||
|
app.UseCors(CorsPolicy);
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Map official MCP protocol endpoints
|
||||||
|
app.MapMcp("/mcp");
|
||||||
|
|
||||||
|
// Root status info endpoint
|
||||||
|
app.MapGet("/", () => Results.Ok(new
|
||||||
|
{
|
||||||
|
Service = "PostFundManagement.Mcp",
|
||||||
|
Status = "Online",
|
||||||
|
Version = "2.3.0",
|
||||||
|
Protocol = "ModelContextProtocol",
|
||||||
|
McpEndpoint = "/mcp",
|
||||||
|
ToolGroupings = new[]
|
||||||
|
{
|
||||||
|
"Grants",
|
||||||
|
"Projects",
|
||||||
|
"Execution",
|
||||||
|
"Governance",
|
||||||
|
"Evidence",
|
||||||
|
"Communications",
|
||||||
|
"Users"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.UseHealthChecks("/health", new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = _ => true,
|
||||||
|
AllowCachingResponses = true,
|
||||||
|
ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"PostFundManagement.Mcp": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:50668;http://localhost:50669"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"EnableServiceCaching": true,
|
||||||
|
"CorsOrigins": [
|
||||||
|
"https://partner.ifri.khongisa.co.za",
|
||||||
|
"https://console.ifri.khongisa.co.za",
|
||||||
|
"https://localhost:7208",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:5174"
|
||||||
|
],
|
||||||
|
"GeneralS3Settings": {
|
||||||
|
"ServiceUrl": "https://s3.garage.khongisa.co.za",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "pfm",
|
||||||
|
"CdnBaseUrl": "https://pfm.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
||||||
|
"ServiceName": "Pfm.MCP"
|
||||||
|
},
|
||||||
|
"SecuritySettings": {
|
||||||
|
"Authority": "https://sts.security.khongisa.co.za",
|
||||||
|
"Audience": "pfm-mcp-dev"
|
||||||
|
},
|
||||||
|
"MachineIdentitySettings": {
|
||||||
|
"Authority": "https://sts.security.khongisa.co.za",
|
||||||
|
"Scope": "skoruba_identity_admin_api"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"ModelContextProtocol": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database": "Error",
|
||||||
|
"Microsoft.EntityFrameworkCore.Update": "Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HasherSettings": {
|
||||||
|
"MinHashLength": 11
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Services.Governance;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Models.Security;
|
||||||
|
using PostFundManagement.Application.Mcp;
|
||||||
|
using PostFundManagement.Application.Abstractions.Mcp;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Tests;
|
||||||
|
|
||||||
|
public class McpToolingTests
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public McpToolingTests()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
services.AddLogging();
|
||||||
|
|
||||||
|
// Register dummy/mock services for testing MCP proxying
|
||||||
|
services.AddSingleton<IGrantsService, TestGrantsService>();
|
||||||
|
services.AddSingleton<IProjectService, TestProjectService>();
|
||||||
|
services.AddSingleton<IExecutionService, TestExecutionService>();
|
||||||
|
services.AddSingleton<IGovernanceService, TestGovernanceService>();
|
||||||
|
services.AddSingleton<IEvidenceService, TestEvidenceService>();
|
||||||
|
services.AddSingleton<ICommunicationsService, TestCommunicationsService>();
|
||||||
|
services.AddSingleton<IUserService, TestUserService>();
|
||||||
|
|
||||||
|
// Register MCP tooling & InApp client
|
||||||
|
services.AddApplicationMcpTools();
|
||||||
|
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddApplicationMcpTools_RegistersAllToolTypesAndInAppClient()
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var sp = scope.ServiceProvider;
|
||||||
|
|
||||||
|
Assert.NotNull(sp.GetService<GrantsMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<ProjectsMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<ExecutionMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<GovernanceMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<EvidenceMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<CommunicationsMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<UsersMcpTools>());
|
||||||
|
Assert.NotNull(sp.GetService<IInAppMcpClient>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InAppMcpClient_DiscoversToolsAcrossAllServiceGroupings()
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<IInAppMcpClient>();
|
||||||
|
|
||||||
|
var tools = client.GetAvailableTools();
|
||||||
|
|
||||||
|
Assert.NotEmpty(tools);
|
||||||
|
Assert.True(tools.Count >= 40, $"Expected at least 40 tools, but found {tools.Count}");
|
||||||
|
|
||||||
|
var groups = tools.Select(t => t.Group).Distinct().OrderBy(g => g).ToList();
|
||||||
|
|
||||||
|
Assert.Contains("Grants", groups);
|
||||||
|
Assert.Contains("Projects", groups);
|
||||||
|
Assert.Contains("Execution", groups);
|
||||||
|
Assert.Contains("Governance", groups);
|
||||||
|
Assert.Contains("Evidence", groups);
|
||||||
|
Assert.Contains("Communications", groups);
|
||||||
|
Assert.Contains("Users", groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InAppMcpClient_ExecutesToolInProcess_WithoutNetworkHops()
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<IInAppMcpClient>();
|
||||||
|
|
||||||
|
var result = await client.ExecuteToolAsync("grants_get_award", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["awardIdHash"] = "awd_test_123"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(result.Success, $"Execution failed: {result.Error}");
|
||||||
|
Assert.NotNull(result.Data);
|
||||||
|
Assert.NotNull(result.SerializedResult);
|
||||||
|
|
||||||
|
var mcpResult = result.Data as Records<Award>;
|
||||||
|
Assert.NotNull(mcpResult);
|
||||||
|
Assert.True(mcpResult.Success);
|
||||||
|
Assert.Equal(1L, mcpResult.Data?.Id);
|
||||||
|
Assert.Equal(1_000_000m, mcpResult.Data?.Amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InAppMcpClient_WhenUnknownTool_ReturnsFailure()
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<IInAppMcpClient>();
|
||||||
|
|
||||||
|
var result = await client.ExecuteToolAsync("non_existent_tool");
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Contains("not recognized", result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void McpToolResult_OptimizesTokenUsage_ForSuccessAndFailure()
|
||||||
|
{
|
||||||
|
var okResult = Records<string>.Ok("TestData");
|
||||||
|
Assert.True(okResult.Success);
|
||||||
|
Assert.Equal("TestData", okResult.Data);
|
||||||
|
Assert.Null(okResult.Errors);
|
||||||
|
|
||||||
|
var failResult = Records<string>.Fail("Something went wrong", ["Error detail 1"]);
|
||||||
|
Assert.False(failResult.Success);
|
||||||
|
Assert.Equal("Something went wrong", failResult.Message);
|
||||||
|
Assert.NotNull(failResult.Errors);
|
||||||
|
Assert.Single(failResult.Errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Test Service Implementations
|
||||||
|
|
||||||
|
private sealed class TestGrantsService : IGrantsService
|
||||||
|
{
|
||||||
|
public Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var award = new Award
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Amount = 1_000_000m
|
||||||
|
};
|
||||||
|
return Task.FromResult(Result.Ok(award));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok(Array.Empty<Award>()));
|
||||||
|
|
||||||
|
public Task<Result> CreateAwardAsync(Application.Services.Grants.CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result> UpdateAwardAsync(Application.Services.Grants.UpdateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok(new Contract { Id = 1 }));
|
||||||
|
|
||||||
|
public Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok(Array.Empty<Contract>()));
|
||||||
|
|
||||||
|
public Task<Result> CreateContractAsync(Application.Services.Grants.CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result> UpdateContractAsync(Application.Services.Grants.UpdateContractRequest request, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result> ExecuteContractAsync(Application.Services.Grants.ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok("https://test.cdn/contract.pdf"));
|
||||||
|
|
||||||
|
public Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok(new Budget { Id = 1 }));
|
||||||
|
|
||||||
|
public Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok(Array.Empty<Budget>()));
|
||||||
|
|
||||||
|
public Task<Result> CreateBudgetAsync(Application.Services.Grants.CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result> UpdateBudgetAsync(Application.Services.Grants.UpdateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
|
||||||
|
public Task<Result> UploadContractAsync(string contractIdHash, Guid userId, Microsoft.AspNetCore.Http.IFormFile file, CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestProjectService : IProjectService
|
||||||
|
{
|
||||||
|
public Task<Result> CreateActivityAsync(Application.Services.Projects.CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateBeneficiaryAsync(Application.Services.Projects.CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateChangeRequestAsync(Application.Services.Projects.CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateComplianceItemAsync(Application.Services.Projects.CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateIssueAsync(Application.Services.Projects.CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateOutcomeAsync(Application.Services.Projects.CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateProgrammeAsync(Application.Services.Projects.CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateProjectAsync(Application.Services.Projects.CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateSiteVisitAsync(Application.Services.Projects.CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Activity>()));
|
||||||
|
public Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Domain.Models.Activity()));
|
||||||
|
public Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Beneficiary>()));
|
||||||
|
public Task<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Beneficiary()));
|
||||||
|
public Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<ChangeRequest>()));
|
||||||
|
public Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new ChangeRequest()));
|
||||||
|
public Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<ComplianceItem>()));
|
||||||
|
public Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new ComplianceItem()));
|
||||||
|
public Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Issue>()));
|
||||||
|
public Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Issue()));
|
||||||
|
public Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Milestone()));
|
||||||
|
public Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Milestone>()));
|
||||||
|
public Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Outcome>()));
|
||||||
|
public Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Outcome()));
|
||||||
|
public Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Programme()));
|
||||||
|
public Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Programme>()));
|
||||||
|
public Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Project>()));
|
||||||
|
public Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Project()));
|
||||||
|
public Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Indicator>()));
|
||||||
|
public Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Milestone>()));
|
||||||
|
public Task<Result<Outcome[]>> GetProjectOutcomesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Outcome>()));
|
||||||
|
public Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Risk>()));
|
||||||
|
public Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Project>()));
|
||||||
|
public Task<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<SiteVisit>()));
|
||||||
|
public Task<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new SiteVisit()));
|
||||||
|
public Task<Result> UpdateActivityAsync(Application.Services.Projects.UpdateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateBeneficiaryAsync(Application.Services.Projects.UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateChangeRequestAsync(Application.Services.Projects.UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateComplianceItemAsync(Application.Services.Projects.UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateIssueAsync(Application.Services.Projects.UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateMilestoneAsync(Application.Services.Projects.UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateOutcomeAsync(Application.Services.Projects.UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateProgrammeAsync(Application.Services.Projects.UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateProjectAsync(Application.Services.Projects.UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateProjectStatusAsync(Application.Services.Projects.UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateSiteVisitAsync(Application.Services.Projects.UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestExecutionService : IExecutionService
|
||||||
|
{
|
||||||
|
public Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateDisbursementAsync(Application.Services.Execution.CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateRiskAsync(Application.Services.Execution.CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateRuleAsync(Application.Services.Execution.CreateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Disbursement()));
|
||||||
|
public Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Disbursement>()));
|
||||||
|
public Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Risk()));
|
||||||
|
public Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Risk>()));
|
||||||
|
public Task<Result<Rule>> GetRuleAsync(string ruleIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Rule()));
|
||||||
|
public Task<Result<Rule[]>> GetRulesAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Rule>()));
|
||||||
|
public Task<Result> UpdateDisbursementAsync(Application.Services.Execution.UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateRiskAsync(Application.Services.Execution.UpdateRiskRequest request, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateRuleAsync(Application.Services.Execution.UpdateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestGovernanceService : IGovernanceService
|
||||||
|
{
|
||||||
|
public Task<Result> CreateInstrumentAsync(Application.Services.Governance.CreateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateOrganisationAsync(Application.Services.Governance.CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreatePortfolioAsync(Application.Services.Governance.CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateTenantAsync(Application.Services.Governance.CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Dashboard(0, 0, 0, 0, 0, 0)));
|
||||||
|
public Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Instrument>()));
|
||||||
|
public Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Organisation()));
|
||||||
|
public Task<Result<Portfolio[]>> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Portfolio>()));
|
||||||
|
public Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Organisation>()));
|
||||||
|
public Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Portfolio()));
|
||||||
|
public Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Programme>()));
|
||||||
|
public Task<Result<decimal>> GetPorfolioValueAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(0m));
|
||||||
|
public Task<Result<Portfolio[]>> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Portfolio>()));
|
||||||
|
public Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Programme>()));
|
||||||
|
public Task<Result<decimal>> GetProgrammeValueAsync(string programmeIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(0m));
|
||||||
|
public Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Tenant()));
|
||||||
|
public Task<Result<TenantOrganisation>> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new TenantOrganisation()));
|
||||||
|
public Task<Result<TenantOrganisation[]>> GetTenantOrganisationsAsync(string tenantIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<TenantOrganisation>()));
|
||||||
|
public Task<Result<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Tenant>()));
|
||||||
|
public Task<Result<TenantUser[]>> GetTenantUsersAsync(string tenantIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<TenantUser>()));
|
||||||
|
public Task<Result> UpdateInstrumentAsync(Application.Services.Governance.UpdateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Organisation>> UpdateOrganisationAsync(Application.Services.Governance.UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Organisation()));
|
||||||
|
public Task<Result> UpdatePortfolioAsync(Application.Services.Governance.UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateTenantAsync(Application.Services.Governance.UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateTenantUserAsync(Application.Services.Governance.UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestEvidenceService : IEvidenceService
|
||||||
|
{
|
||||||
|
public Task<Result> CreateEvidenceAsync(Guid userId, Microsoft.AspNetCore.Http.IFormCollection form, Microsoft.AspNetCore.Http.IFormFile file, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateIndicatorAsync(Application.Services.Evidence.CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateMilestoneAsync(Application.Services.Evidence.CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Domain.Models.Evidence()));
|
||||||
|
public Task<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Evidence>()));
|
||||||
|
public Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok("https://test.cdn/evidence.pdf"));
|
||||||
|
public Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) => Task.FromResult(Result.Ok(new Indicator()));
|
||||||
|
public Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Indicator>()));
|
||||||
|
public Task<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Evidence>()));
|
||||||
|
public Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Evidence>()));
|
||||||
|
public Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, decimal? actualCost, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateEvidenceAsync(Application.Services.Evidence.UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestCommunicationsService : ICommunicationsService
|
||||||
|
{
|
||||||
|
public Task<Result> CreateNotificationAsync(Application.Services.Communications.CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<ApplicantData[]>> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<ApplicantData>()));
|
||||||
|
public Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Invite()));
|
||||||
|
public Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Invite>()));
|
||||||
|
public Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Invite>()));
|
||||||
|
public Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Notification()));
|
||||||
|
public Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Notification>()));
|
||||||
|
public Task<Result<Notification[]>> GetUserNotificationsAsync(Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Notification>()));
|
||||||
|
public Task<Result<Invite>> SendInviteAsync(Application.Services.Communications.InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new Invite()));
|
||||||
|
public Task<Result> UpdateNotificationAsync(Application.Services.Communications.UpdateNotificationRequest request, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestUserService : IUserService
|
||||||
|
{
|
||||||
|
public Task<Result> AddUserClaimAsync(AddUserClaimRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> AddUserRoleAsync(Guid targetUserId, Guid roleId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> CreateUserAsync(string tenantIdHash, string idNumberHash, CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> DeleteUserClaimAsync(string claimIdHash, Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> DeleteUserRoleAsync(Guid targetUserId, Guid roleId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result<Domain.Models.Security.Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Security.Claim>()));
|
||||||
|
public Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Role>()));
|
||||||
|
public Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(new User { Id = userId }));
|
||||||
|
public Task<Result<Domain.Models.Security.Claim[]>> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Domain.Models.Security.Claim>()));
|
||||||
|
public Task<Result<Role[]>> GetUserRolesAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<Role>()));
|
||||||
|
public Task<Result<User[]>> GetUsersAsync(Pagination pagination, Application.Services.Users.UserFilterRequest request, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok(Array.Empty<User>()));
|
||||||
|
public Task<Result> LogoutUserAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> SetUserLockoutAsync(Guid targetUserId, Guid userId, bool disable, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> SyncUserAsync(Guid userId, string email, string role, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
public Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) => Task.FromResult(Result.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
+75
-9
@@ -1,13 +1,4 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Folder Name="/Presentation/">
|
|
||||||
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" />
|
|
||||||
<Project Path="PostFundManagement.Jobs/PostFundManagement.Jobs.csproj" Id="81281806-b5ca-4784-87af-3e8feeac988c" />
|
|
||||||
</Folder>
|
|
||||||
<Folder Name="/Shared/">
|
|
||||||
<Project Path="PostFundManagement.Application/PostFundManagement.Application.csproj" Id="750c7754-53a7-4573-abed-5c436246a04d" />
|
|
||||||
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" />
|
|
||||||
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
|
|
||||||
</Folder>
|
|
||||||
<Folder Name="/Solution Items/">
|
<Folder Name="/Solution Items/">
|
||||||
<File Path=".editorconfig" />
|
<File Path=".editorconfig" />
|
||||||
<File Path=".gitignore" />
|
<File Path=".gitignore" />
|
||||||
@@ -18,6 +9,81 @@
|
|||||||
<File Path="PostFundManagement.snk" />
|
<File Path="PostFundManagement.snk" />
|
||||||
<File Path="README.md" />
|
<File Path="README.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Backend/">
|
||||||
|
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" />
|
||||||
|
<Project Path="PostFundManagement.Jobs/PostFundManagement.Jobs.csproj" Id="81281806-b5ca-4784-87af-3e8feeac988c" />
|
||||||
|
<Project Path="PostFundManagement.Mcp/PostFundManagement.Mcp.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Shared/">
|
||||||
|
<Project Path="PostFundManagement.Application/PostFundManagement.Application.csproj" Id="750c7754-53a7-4573-abed-5c436246a04d" />
|
||||||
|
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" />
|
||||||
|
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Websites/">
|
||||||
|
<Project Path="PostFundManagement.ConsoleUI/" Type="Website" DisplayName="PostFundManagement.ConsoleUI">
|
||||||
|
<BuildType Project="Debug" />
|
||||||
|
<Properties Name="WebsiteProperties">
|
||||||
|
<Property Name="Debug.AspNetCompiler.Debug" Value=""True"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.ConsoleUI\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51939\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.VirtualPath" Value=""/localhost_51939"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Debug" Value=""False"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.ConsoleUI\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51939\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.VirtualPath" Value=""/localhost_51939"" />
|
||||||
|
<Property Name="TargetFrameworkMoniker" Value="".NETFramework,Version%3Dv4.0"" />
|
||||||
|
<Property Name="VWDPort" Value=""51939"" />
|
||||||
|
</Properties>
|
||||||
|
</Project>
|
||||||
|
<Project Path="PostFundManagement.PartnerUI/" Type="Website" DisplayName="PostFundManagement.PartnerUI">
|
||||||
|
<BuildType Project="Debug" />
|
||||||
|
<Properties Name="WebsiteProperties">
|
||||||
|
<Property Name="Debug.AspNetCompiler.Debug" Value=""True"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.PartnerUI\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51524\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.VirtualPath" Value=""/localhost_51524"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Debug" Value=""False"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.PartnerUI\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51524\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.VirtualPath" Value=""/localhost_51524"" />
|
||||||
|
<Property Name="TargetFrameworkMoniker" Value="".NETFramework,Version%3Dv4.0"" />
|
||||||
|
<Property Name="VWDPort" Value=""51524"" />
|
||||||
|
</Properties>
|
||||||
|
</Project>
|
||||||
|
<Project Path="PostFundManagement.ShowcaseUI/" Type="Website" DisplayName="PostFundManagement.ShowcaseUI">
|
||||||
|
<BuildType Project="Debug" />
|
||||||
|
<Properties Name="WebsiteProperties">
|
||||||
|
<Property Name="Debug.AspNetCompiler.Debug" Value=""True"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.ShowcaseUI\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51546\"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Debug.AspNetCompiler.VirtualPath" Value=""/localhost_51546"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Debug" Value=""False"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.FixedNames" Value=""false"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.ForceOverwrite" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.PhysicalPath" Value=""PostFundManagement.ShowcaseUI\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.TargetPath" Value=""PrecompiledWeb\localhost_51546\"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.Updateable" Value=""true"" />
|
||||||
|
<Property Name="Release.AspNetCompiler.VirtualPath" Value=""/localhost_51546"" />
|
||||||
|
<Property Name="TargetFrameworkMoniker" Value="".NETFramework,Version%3Dv4.0"" />
|
||||||
|
<Property Name="VWDPort" Value=""51546"" />
|
||||||
|
</Properties>
|
||||||
|
</Project>
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Tests/">
|
<Folder Name="/Tests/">
|
||||||
<Project Path="PostFundManagement.Tests/PostFundManagement.Tests.csproj" Id="ce650d27-8a68-436d-b5c1-db25f2286e85" />
|
<Project Path="PostFundManagement.Tests/PostFundManagement.Tests.csproj" Id="ce650d27-8a68-436d-b5c1-db25f2286e85" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|||||||
@@ -41,7 +41,14 @@ The application follows a modular, hierarchical architecture where high-level fu
|
|||||||
### **5. Notifications & Platform Services**
|
### **5. Notifications & Platform Services**
|
||||||
* Multi-channel delivery engine (`NotificationPlatform`) tracking candidate invitations, reminders, and status updates (`NotificationStatus`) across Email, SMS, WhatsApp, In-App, and Webhooks.
|
* Multi-channel delivery engine (`NotificationPlatform`) tracking candidate invitations, reminders, and status updates (`NotificationStatus`) across Email, SMS, WhatsApp, In-App, and Webhooks.
|
||||||
|
|
||||||
### **6. Background Processing & Distributed Jobs Engine**
|
### **6. Model Context Protocol (MCP) — In-App & External Tooling**
|
||||||
|
* **Central Tool Definitions (`PostFundManagement.Application/Mcp`)**: Tools proxy core domain services organized by business domain (`Grants`, `Projects`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`).
|
||||||
|
* **In-App MCP (`PostFundManagement.Api`)**: The API has direct in-process access to tooling via `IInAppMcpClient` without making external web or HTTP calls.
|
||||||
|
* **External MCP Server (`PostFundManagement.Mcp`)**: Dedicated .NET 10 host serving MCP tools over HTTP/SSE (`/mcp`) and Stdio (`--stdio`) for external agents, Claude Desktop, and IDEs.
|
||||||
|
* **Token Optimization**: Standardized `McpToolResult` models minimize prompt token overhead.
|
||||||
|
* **Agent Status**: Tooling infrastructure is established; agent definitions are deferred to future milestones.
|
||||||
|
|
||||||
|
### **7. Background Processing & Distributed Jobs Engine**
|
||||||
* **`PostFundManagement.Jobs`**: Dedicated background execution host powered by **Quartz.NET (v4.6+)** with a PostgreSQL clustered JobStore (`pfm-scheduler`).
|
* **`PostFundManagement.Jobs`**: Dedicated background execution host powered by **Quartz.NET (v4.6+)** with a PostgreSQL clustered JobStore (`pfm-scheduler`).
|
||||||
* **Decoupled Architecture**: `PostFundManagement.Api` serves strictly as a scheduler client (`ZeroSizeThreadPool`), dispatching events into the cluster while leaving execution exclusively to `PostFundManagement.Jobs` (`MaxConcurrency = 5`).
|
* **Decoupled Architecture**: `PostFundManagement.Api` serves strictly as a scheduler client (`ZeroSizeThreadPool`), dispatching events into the cluster while leaving execution exclusively to `PostFundManagement.Jobs` (`MaxConcurrency = 5`).
|
||||||
* **Event-Driven Execution**: Domain events implementing `IEvent : INotification` are mediated and serialized into `MediatorJob<TNotification>`.
|
* **Event-Driven Execution**: Domain events implementing `IEvent : INotification` are mediated and serialized into `MediatorJob<TNotification>`.
|
||||||
|
|||||||
@@ -123,6 +123,63 @@ services:
|
|||||||
# Redis overrides
|
# Redis overrides
|
||||||
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}
|
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
pfm-mcp:
|
||||||
|
image: pfm-mcp
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./PostFundManagement.Mcp/Dockerfile
|
||||||
|
network: host
|
||||||
|
container_name: pfm-mcp
|
||||||
|
ports:
|
||||||
|
- "8079:8080"
|
||||||
|
environment:
|
||||||
|
# Header forwarding overrides
|
||||||
|
- ASPNETCORE_ENVIRONMENT=${ENVIRONMENT:-Development}
|
||||||
|
- ForwardedHeadersConfiguration__Enabled=true
|
||||||
|
- ForwardedHeadersConfiguration__AllowAll=true
|
||||||
|
- ForwardedHeadersConfiguration__ForwardLimit=1
|
||||||
|
|
||||||
|
# CORS Overrides
|
||||||
|
- CorsOrigins__0=${CORS_ORIGIN_CONSOLE:-https://console.ifri.khongisa.co.za}
|
||||||
|
- CorsOrigins__1=${CORS_ORIGIN_API:-https://api.ifri.khongisa.co.za}
|
||||||
|
- CorsOrigins__2=${CORS_ORIGIN_LOCAL:-https://localhost:7208}
|
||||||
|
- CorsOrigins__3=${CORS_ORIGIN_PARTNER:-https://partner.ifri.khongisa.co.za}
|
||||||
|
- CorsOrigins__4=${CORS_ORIGIN_SHOWCASE:-https://showcase.ifri.khongisa.co.za}
|
||||||
|
|
||||||
|
# Postgres Database Overrides
|
||||||
|
- ConnectionStrings__PfmDatabase=Server=${POSTGRES_HOST:-192.168.1.170};Port=${POSTGRES_PORT:-5432};Database=${POSTGRES_DB:-pfm};User Id=${POSTGRES_USER:-pfm-user};Password=${POSTGRES_PASSWORD:-};
|
||||||
|
|
||||||
|
# Garage S3 Settings
|
||||||
|
- GeneralS3Settings__ServiceUrl=${GENERAL_S3_SERVICE_URL:-https://s3.garage.khongisa.co.za}
|
||||||
|
- GeneralS3Settings__Region=${GENERAL_S3_REGION:-garage}
|
||||||
|
- GeneralS3Settings__BucketName=${GENERAL_S3_BUCKET_NAME:-pfm}
|
||||||
|
- GeneralS3Settings__CdnBaseUrl=${GENERAL_S3_CDN_BASE_URL:-https://pfm.cdn.khongisa.co.za}
|
||||||
|
- GeneralS3Settings__AccessKey=${GENERAL_S3_ACCESS_KEY:-}
|
||||||
|
- GeneralS3Settings__SecretKey=${GENERAL_S3_SECRET_KEY:-}
|
||||||
|
|
||||||
|
# HashIds overrides
|
||||||
|
- HasherSettings__MinHashLength=${HASHER_MIN_HASH_LENGTH:-11}
|
||||||
|
- HasherSettings__Salt=${HASHER_SALT:-}
|
||||||
|
|
||||||
|
# Skoruba Duende Administration Security Overrides
|
||||||
|
- ConnectionStrings__SkorubaApi=${SKORUBA_ADMIN_API:-https://api.security.khongisa.co.za/}
|
||||||
|
- MachineIdentitySettings__Authority=${ADMIN_SECURITY_AUTHORITY:-https://sts.security.khongisa.co.za}
|
||||||
|
- MachineIdentitySettings__Scope=${ADMIN_SECURITY_SCOPE:-}
|
||||||
|
- MachineIdentitySettings__ClientId=${ADMIN_SECURITY_CLIENT_ID:-}
|
||||||
|
- MachineIdentitySettings__ClientSecret=${ADMIN_SECURITY_CLIENT_SECRET:-}
|
||||||
|
|
||||||
|
# Data Protection Overrides
|
||||||
|
- DataProtection__Certificate=${DATA_PROTECTION_CERTIFICATE:-}
|
||||||
|
- DataProtection__Password=${DATA_PROTECTION_CERTIFICATE_PASSWORD:-}
|
||||||
|
|
||||||
|
# Monitoring Overrides
|
||||||
|
- Monitoring__Address=${MONITORING_ADDRESS:-http://aspire-dashboard-service.aspire.svc.cluster.local:18889}
|
||||||
|
- Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.MCP}
|
||||||
|
- Monitoring__ApiKey=${MONITORING_API_KEY:-}
|
||||||
|
|
||||||
|
# Redis overrides
|
||||||
|
- ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}
|
||||||
|
restart: unless-stopped
|
||||||
pfm-console:
|
pfm-console:
|
||||||
image: pfm-console
|
image: pfm-console
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user