Skip to content

Commit 445bdb0

Browse files
authored
Tool Management (#39)
add Tool registration management service to register and deploy containerized tools
1 parent 4ec466c commit 445bdb0

30 files changed

+2597
-57
lines changed

deployment/k8s/local-deployment.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,57 @@ roleRef:
7676
kind: Role
7777
name: app-manager
7878
apiGroup: rbac.authorization.k8s.io
79+
---
80+
apiVersion: v1
81+
kind: Service
82+
metadata:
83+
name: redis-service
84+
namespace: adapter
85+
spec:
86+
type: ClusterIP
87+
ports:
88+
- port: 6379
89+
targetPort: 6379
90+
protocol: TCP
91+
selector:
92+
app: redis
93+
---
94+
apiVersion: apps/v1
95+
kind: Deployment
96+
metadata:
97+
name: redis
98+
namespace: adapter
99+
spec:
100+
replicas: 1
101+
selector:
102+
matchLabels:
103+
app: redis
104+
template:
105+
metadata:
106+
labels:
107+
app: redis
108+
spec:
109+
containers:
110+
- name: redis
111+
image: redis:7-alpine
112+
ports:
113+
- containerPort: 6379
114+
resources:
115+
requests:
116+
cpu: 100m
117+
memory: 128Mi
118+
limits:
119+
cpu: 500m
120+
memory: 512Mi
121+
livenessProbe:
122+
tcpSocket:
123+
port: 6379
124+
initialDelaySeconds: 30
125+
periodSeconds: 10
126+
readinessProbe:
127+
exec:
128+
command:
129+
- redis-cli
130+
- ping
131+
initialDelaySeconds: 5
132+
periodSeconds: 5

dotnet/Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0" />
99
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.51.0" />
1010
<PackageVersion Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />
11-
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
11+
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.10" />
12+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
1213
<PackageVersion Include="Microsoft.Identity.Web" Version="3.9.1" />
1314
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
1415
<PackageVersion Include="Moq" Version="4.20.72" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.McpGateway.Management.Contracts
5+
{
6+
/// <summary>
7+
/// Defines the type of MCP resource being deployed.
8+
/// </summary>
9+
public enum ResourceType
10+
{
11+
/// <summary>
12+
/// Represents an adapter resource.
13+
/// </summary>
14+
Mcp,
15+
16+
/// <summary>
17+
/// Represents a tool resource.
18+
/// </summary>
19+
Tool
20+
}
21+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.McpGateway.Management.Contracts
7+
{
8+
/// <summary>
9+
/// Represents the data for a tool deployment, including image details and the tool definition.
10+
/// </summary>
11+
public class ToolData : AdapterData
12+
{
13+
/// <summary>
14+
/// The MCP tool definition as JSON.
15+
/// This contains the tool's name, description, input schema, etc.
16+
/// </summary>
17+
[JsonPropertyOrder(10)]
18+
public required ToolDefinition ToolDefinition { get; set; }
19+
20+
public ToolData(
21+
string name,
22+
string imageName,
23+
string imageVersion,
24+
ToolDefinition toolDefinition,
25+
Dictionary<string, string>? environmentVariables = null,
26+
int? replicaCount = 1,
27+
string description = "",
28+
bool useWorkloadIdentity = false)
29+
: base(name, imageName, imageVersion, environmentVariables, replicaCount, description, useWorkloadIdentity)
30+
{
31+
if (name != toolDefinition.Name)
32+
{
33+
throw new ArgumentException("Tool name in ToolData must match the name in ToolDefinition.");
34+
}
35+
36+
ToolDefinition = toolDefinition;
37+
}
38+
39+
public ToolData() { }
40+
}
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using ModelContextProtocol.Protocol;
6+
7+
namespace Microsoft.McpGateway.Management.Contracts
8+
{
9+
/// <summary>
10+
/// Represents an MCP tool definition.
11+
/// Combines the MCP Tool contract with execution metadata.
12+
/// </summary>
13+
public class ToolDefinition
14+
{
15+
/// <summary>
16+
/// The MCP Tool definition (contains name, description, input schema).
17+
/// </summary>
18+
[JsonPropertyName("tool")]
19+
public required Tool Tool { get; set; }
20+
21+
/// <summary>
22+
/// The unique name of the tool (derived from Tool.Name for convenience).
23+
/// </summary>
24+
[JsonIgnore]
25+
public string Name => Tool.Name;
26+
27+
/// <summary>
28+
/// The port for the execution service endpoint.
29+
/// Defaults to 443.
30+
/// </summary>
31+
[JsonPropertyName("port")]
32+
public int Port { get; set; } = 443;
33+
34+
/// <summary>
35+
/// The path for the execution service endpoint.
36+
/// Defaults to "/score".
37+
/// </summary>
38+
[JsonPropertyName("path")]
39+
public string Path { get; set; } = "/score";
40+
}
41+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.McpGateway.Management.Contracts
8+
{
9+
/// <summary>
10+
/// Represents a tool resource with metadata.
11+
/// </summary>
12+
public class ToolResource : ToolData
13+
{
14+
[JsonPropertyOrder(-1)]
15+
[JsonPropertyName("id")]
16+
public required string Id { get; set; }
17+
18+
/// <summary>
19+
/// The ID of the user who created the tool.
20+
/// </summary>
21+
[JsonPropertyOrder(30)]
22+
public required string CreatedBy { get; set; }
23+
24+
/// <summary>
25+
/// The date and time when the tool was created.
26+
/// </summary>
27+
[JsonPropertyOrder(31)]
28+
public DateTimeOffset CreatedAt { get; set; }
29+
30+
/// <summary>
31+
/// The date and time when the tool was last updated.
32+
/// </summary>
33+
[JsonPropertyOrder(32)]
34+
public DateTimeOffset LastUpdatedAt { get; set; }
35+
36+
public static ToolResource Create(ToolData data, string createdBy, DateTimeOffset createdAt) =>
37+
new()
38+
{
39+
Id = data.Name,
40+
Name = data.Name,
41+
ImageName = data.ImageName,
42+
ImageVersion = data.ImageVersion,
43+
EnvironmentVariables = data.EnvironmentVariables,
44+
ReplicaCount = data.ReplicaCount,
45+
Description = data.Description,
46+
UseWorkloadIdentity = data.UseWorkloadIdentity,
47+
ToolDefinition = data.ToolDefinition,
48+
CreatedBy = createdBy,
49+
CreatedAt = createdAt,
50+
LastUpdatedAt = DateTimeOffset.UtcNow,
51+
};
52+
53+
public ToolResource() { }
54+
}
55+
}

dotnet/Microsoft.McpGateway.Management/src/Deployment/IAdapterDeploymentManager.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ public interface IAdapterDeploymentManager
1414
/// Creates a new deployment asynchronously.
1515
/// </summary>
1616
/// <param name="request">The data for the adapter deployment.</param>
17+
/// <param name="resourceType">The type of resource being deployed (Adapter or Tool).</param>
1718
/// <param name="cancellationToken">Token to cancel the operation.</param>
18-
Task CreateDeploymentAsync(AdapterData request, CancellationToken cancellationToken);
19+
Task CreateDeploymentAsync(AdapterData request, ResourceType resourceType, CancellationToken cancellationToken);
1920

2021
/// <summary>
2122
/// Deletes an existing deployment asynchronously.
@@ -36,6 +37,7 @@ public interface IAdapterDeploymentManager
3637
/// Get the deployed adapter deployment logs.
3738
/// </summary>
3839
/// <param name="name">The name of the adapter deployment</param>
40+
/// <param name="ordinal">The ordinal index of the pod instance.</param>
3941
/// <param name="cancellationToken">Token to cancel the operation.</param>
4042
/// <returns>The logs from the deployment pod.</returns>
4143
Task<string> GetDeploymentLogsAsync(string name, int ordinal, CancellationToken cancellationToken);
@@ -44,7 +46,8 @@ public interface IAdapterDeploymentManager
4446
/// Updates an existing deployment asynchronously.
4547
/// </summary>
4648
/// <param name="request">The data for the adapter deployment update.</param>
49+
/// <param name="resourceType">The type of resource being deployed (Adapter or Tool).</param>
4750
/// <param name="cancellationToken">Token to cancel the operation.</param>
48-
Task UpdateDeploymentAsync(AdapterData request, CancellationToken cancellationToken);
51+
Task UpdateDeploymentAsync(AdapterData request, ResourceType resourceType, CancellationToken cancellationToken);
4952
}
5053
}

dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ public KubernetesAdapterDeploymentManager(string containerRegistryAddress, IKube
2727
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
2828
}
2929

30-
public async Task CreateDeploymentAsync(AdapterData request, CancellationToken cancellationToken)
30+
public async Task CreateDeploymentAsync(AdapterData request, ResourceType resourceType, CancellationToken cancellationToken)
3131
{
3232
var labels = new Dictionary<string, string>
3333
{
34-
{ $"{AdapterNamespace}/type", "mcp" },
34+
{ $"{AdapterNamespace}/type", resourceType.ToString().ToLowerInvariant() },
3535
{ $"{AdapterNamespace}/name", request.Name },
3636
{ "azure.workload.identity/use", request.UseWorkloadIdentity.ToString().ToLowerInvariant() }
3737
};
@@ -97,6 +97,8 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
9797
}
9898
};
9999

100+
// For tools: use ClusterIP (default, stateless routing)
101+
// For adapters: use headless service (ClusterIP = None, stateful routing)
100102
var service = new V1Service
101103
{
102104
Metadata = new V1ObjectMeta
@@ -105,7 +107,7 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
105107
},
106108
Spec = new V1ServiceSpec
107109
{
108-
ClusterIP = "None",
110+
ClusterIP = resourceType == ResourceType.Tool ? null : "None",
109111
Selector = labels,
110112
Ports =
111113
[
@@ -119,7 +121,7 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
119121
}
120122
};
121123

122-
_logger.LogInformation("Creating deployment for {name}.", request.Name.Sanitize());
124+
_logger.LogInformation("Creating deployment for {name} with resource type {resourceType}.", request.Name.Sanitize(), resourceType.ToString().ToLowerInvariant());
123125
try
124126
{
125127
await _kubeClient.UpsertStatefulSetAsync(statefulSet, AdapterNamespace, cancellationToken).ConfigureAwait(false);
@@ -133,24 +135,34 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
133135
try
134136
{
135137
await _kubeClient.UpsertServiceAsync(service, AdapterNamespace, cancellationToken).ConfigureAwait(false);
136-
_logger.LogInformation("Submitted Kubernetes service for {name}.", request.Name.Sanitize());
138+
_logger.LogInformation("Submitted Kubernetes service for {name} with {serviceType} routing.", request.Name.Sanitize(), resourceType == ResourceType.Tool ? "stateless (ClusterIP)" : "stateful (headless)");
137139
}
138140
catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.Conflict)
139141
{
140142
_logger.LogInformation("Kubernetes service for {name} already exists. Skip service creation.", request.Name.Sanitize());
141143
}
142144
}
143145

144-
public async Task UpdateDeploymentAsync(AdapterData request, CancellationToken cancellationToken)
146+
public async Task UpdateDeploymentAsync(AdapterData request, ResourceType resourceType, CancellationToken cancellationToken)
145147
{
146148
var statefulSet = await _kubeClient.ReadStatefulSetAsync(request.Name, AdapterNamespace, cancellationToken).ConfigureAwait(false);
149+
147150
var patch = new
148151
{
149152
spec = new
150153
{
151154
replicas = request.ReplicaCount,
152155
template = new
153156
{
157+
metadata = new
158+
{
159+
labels = new Dictionary<string, string>
160+
{
161+
{ $"{AdapterNamespace}/type", resourceType.ToString().ToLowerInvariant() },
162+
{ $"{AdapterNamespace}/name", request.Name },
163+
{ "azure.workload.identity/use", request.UseWorkloadIdentity.ToString().ToLowerInvariant() }
164+
}
165+
},
154166
spec = new
155167
{
156168
containers = new[]
@@ -168,7 +180,7 @@ public async Task UpdateDeploymentAsync(AdapterData request, CancellationToken c
168180
};
169181

170182
var patchContent = new V1Patch(JsonSerializer.Serialize(patch), V1Patch.PatchType.StrategicMergePatch);
171-
_logger.LogInformation("Updating deployment for {name}.", request.Name.Sanitize());
183+
_logger.LogInformation("Updating deployment for {name} with resource type {resourceType}.", request.Name.Sanitize(), resourceType.ToString().ToLowerInvariant());
172184
await _kubeClient.PatchStatefulSetAsync(patchContent, request.Name, AdapterNamespace, cancellationToken).ConfigureAwait(false);
173185
_logger.LogInformation("Submitted updating deployment for {name}.", request.Name.Sanitize());
174186
}

dotnet/Microsoft.McpGateway.Management/src/Microsoft.McpGateway.Management.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
<ItemGroup>
1717
<PackageReference Include="KubernetesClient" />
1818
<PackageReference Include="Microsoft.Azure.Cosmos" />
19+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" />
1920
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
21+
<PackageReference Include="ModelContextProtocol" />
2022
<PackageReference Include="Newtonsoft.Json" />
2123
</ItemGroup>
2224
</Project>

dotnet/Microsoft.McpGateway.Management/src/Service/AdapterManagementService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// -// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System.Security.Claims;
@@ -38,7 +38,7 @@ public async Task<AdapterResource> CreateAsync(ClaimsPrincipal accessContext, Ad
3838
logger.LogInformation("Start creating /adapters/{name}.", request.Name.Sanitize());
3939

4040
logger.LogInformation("Start kubernetes deployment for /adapters/{name}.", request.Name.Sanitize());
41-
await _deploymentManager.CreateDeploymentAsync(request, cancellationToken).ConfigureAwait(false);
41+
await _deploymentManager.CreateDeploymentAsync(request, ResourceType.Mcp, cancellationToken).ConfigureAwait(false);
4242

4343
logger.LogInformation("Start update internal storage for /adapters/{name}.", request.Name.Sanitize());
4444
await _store.UpsertAsync(adapter, cancellationToken).ConfigureAwait(false);
@@ -81,7 +81,7 @@ public async Task<AdapterResource> UpdateAsync(ClaimsPrincipal accessContext, Ad
8181
existing.ImageVersion != request.ImageVersion ||
8282
!existing.EnvironmentVariables.OrderBy(kv => kv.Key).SequenceEqual(request.EnvironmentVariables.OrderBy(kv => kv.Key)))
8383
{
84-
await _deploymentManager.UpdateDeploymentAsync(updated, cancellationToken).ConfigureAwait(false);
84+
await _deploymentManager.UpdateDeploymentAsync(updated, ResourceType.Mcp, cancellationToken).ConfigureAwait(false);
8585
}
8686

8787
await _store.UpsertAsync(updated, cancellationToken).ConfigureAwait(false);

0 commit comments

Comments
 (0)