Add Kubernetes and Jira MCP tools with auto-registration
This commit is contained in:
256
AGENTS.md
256
AGENTS.md
@@ -1,245 +1,35 @@
|
|||||||
## AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
### PRIORITY
|
## Scope
|
||||||
|
|
||||||
1. User request
|
- `LazyBearWorks.sln` builds exactly one project: `LazyBear.MCP/`. `Libraries/Confluence/` is present in the repo but is not part of the solution and has no `.csproj`; ignore it unless the user explicitly asks for it.
|
||||||
2. This file
|
|
||||||
3. Existing code
|
|
||||||
|
|
||||||
---
|
## Source Of Truth
|
||||||
|
|
||||||
## CODE
|
- Trust `LazyBear.MCP/Program.cs` and project config over `README.md`. The README describes Razor Pages/UI and a broader tool surface, but the current app only configures MCP HTTP transport and auto-registers tool classes from the assembly.
|
||||||
|
|
||||||
### STACK
|
## Entry Points
|
||||||
|
|
||||||
* .NET / C#
|
- Main app entrypoint: `LazyBear.MCP/Program.cs`.
|
||||||
* ASP.NET Core
|
- Live MCP tools are discovered via `AddMcpServer().WithHttpTransport().WithToolsFromAssembly()`. Current tool classes are under `LazyBear.MCP/Services/Kubernetes/` and `LazyBear.MCP/Services/Jira/` and use `[McpServerToolType]`.
|
||||||
* MCP
|
- Do not assume Razor Pages are active just because `Pages/` exists: `Program.cs` does not call `AddRazorPages()` or `MapRazorPages()`.
|
||||||
|
|
||||||
### STRUCTURE
|
## Commands
|
||||||
|
|
||||||
* `Server/` — endpoints
|
- Build from repo root: `dotnet build`
|
||||||
* `Services/` — business logic
|
- Build project directly: `dotnet build "LazyBear.MCP/LazyBear.MCP.csproj"`
|
||||||
* `Tools/` — MCP tools
|
- Run from repo root: `dotnet run --project "LazyBear.MCP"`
|
||||||
|
- MCP manual check: `npx @modelcontextprotocol/inspector dotnet run --project LazyBear.MCP`
|
||||||
|
- There are no test projects in the solution right now; default verification is `dotnet build`, plus a focused run/inspector check for transport or tool-registration changes.
|
||||||
|
|
||||||
### RULES
|
## Runtime Quirks
|
||||||
|
|
||||||
**Before edit**
|
- `LazyBear.MCP/Properties/launchSettings.json` says `http://localhost:5079`, but the app actually listens on `http://localhost:5000` because `Program.cs` hardcodes `app.Run("http://localhost:5000")`.
|
||||||
|
- SDK pin is in `LazyBear.MCP/global.json` (`10.0.100`), not at the repo root.
|
||||||
|
|
||||||
* Read related code
|
## Config Gotchas
|
||||||
* Reuse existing patterns
|
|
||||||
* Do not over-engineer
|
|
||||||
|
|
||||||
**After edit**
|
- App config lives in `LazyBear.MCP/appsettings.json`.
|
||||||
|
- `Kubernetes:KubeconfigPath` defaults empty. `K8sClientFactory` then tries the default kubeconfig, then in-cluster config.
|
||||||
* Run:
|
- Jira tools require `Jira:Url`; if it is empty, `JiraClientFactory` throws during provider setup. The app still starts because providers capture init errors and tools return those errors as strings.
|
||||||
|
- Treat `appsettings.json` values as potentially sensitive; do not commit real Jira tokens or cluster-specific settings.
|
||||||
```
|
|
||||||
dotnet build
|
|
||||||
```
|
|
||||||
* Build must succeed
|
|
||||||
* Do not break MCP protocol
|
|
||||||
* Keep diff minimal
|
|
||||||
|
|
||||||
**Style**
|
|
||||||
|
|
||||||
* Match existing style
|
|
||||||
* Avoid duplication
|
|
||||||
* Prefer small changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMMUNICATION
|
|
||||||
|
|
||||||
### LANGUAGE
|
|
||||||
|
|
||||||
* Output: Russian
|
|
||||||
* Code: English
|
|
||||||
* Comments/commits: Russian
|
|
||||||
|
|
||||||
### BEHAVIOR
|
|
||||||
|
|
||||||
* Be concise
|
|
||||||
* Do not explain obvious things
|
|
||||||
* Do not produce long texts
|
|
||||||
* Prefer action when no clarification is needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### QUESTIONS
|
|
||||||
|
|
||||||
Default:
|
|
||||||
|
|
||||||
* If result can be improved by user choice → ASK FIRST
|
|
||||||
* Do not execute immediately if preferences affect result
|
|
||||||
|
|
||||||
Ask BEFORE action if:
|
|
||||||
|
|
||||||
* Multiple valid directions exist
|
|
||||||
* Result depends on user preference
|
|
||||||
* Request is broad (e.g. "suggest", "recommend", "generate")
|
|
||||||
|
|
||||||
Otherwise:
|
|
||||||
|
|
||||||
* Proceed with best reasonable assumption
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### QUESTION TOOL
|
|
||||||
|
|
||||||
`question` is the UI tool for user choices in OpenCode.
|
|
||||||
|
|
||||||
Use `question` BEFORE answering when:
|
|
||||||
|
|
||||||
* 2+ meaningful options exist
|
|
||||||
* clarification improves result quality
|
|
||||||
* choice affects architecture, config, data, or output
|
|
||||||
|
|
||||||
Do NOT skip `question` in these cases.
|
|
||||||
|
|
||||||
Do NOT use when:
|
|
||||||
|
|
||||||
* request is already specific
|
|
||||||
* only one valid answer exists
|
|
||||||
* clarification does not change result
|
|
||||||
|
|
||||||
If unavailable:
|
|
||||||
|
|
||||||
* ask in plain text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### RESTRICTIONS
|
|
||||||
|
|
||||||
* Do not end with only a question
|
|
||||||
* Do not expose secrets
|
|
||||||
* Do not repeat user text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TOOLS
|
|
||||||
|
|
||||||
Always assume tools MAY be available.
|
|
||||||
|
|
||||||
Before solving:
|
|
||||||
|
|
||||||
* Identify relevant tools
|
|
||||||
* Prefer tools when they simplify the task
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
* Do not invent tools
|
|
||||||
* Use only confirmed available tools
|
|
||||||
* If availability unclear → proceed without them
|
|
||||||
|
|
||||||
Tools are part of the solution, not optional.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MEMORY
|
|
||||||
|
|
||||||
Use ONLY if memory tools are available.
|
|
||||||
|
|
||||||
### READ FIRST
|
|
||||||
|
|
||||||
Before coding or assumptions:
|
|
||||||
|
|
||||||
1. Try `read_graph`
|
|
||||||
2. Then `search_nodes()`
|
|
||||||
3. Avoid duplicate observations
|
|
||||||
|
|
||||||
If unavailable:
|
|
||||||
|
|
||||||
* Skip memory usage
|
|
||||||
|
|
||||||
### KEY FORMAT
|
|
||||||
|
|
||||||
```text
|
|
||||||
lazybear/<type>/<name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```text
|
|
||||||
lazybear/bug/auth-fail
|
|
||||||
lazybear/decision/mcp-timeout
|
|
||||||
lazybear/config/jira-base-url
|
|
||||||
```
|
|
||||||
|
|
||||||
### ALLOWED TYPES
|
|
||||||
|
|
||||||
* `architecture`
|
|
||||||
* `mcp_tool`
|
|
||||||
* `decision`
|
|
||||||
* `bug`
|
|
||||||
* `config`
|
|
||||||
* `task_log`
|
|
||||||
|
|
||||||
### WRITE ONLY WHEN USEFUL
|
|
||||||
|
|
||||||
* architecture changes → `architecture`
|
|
||||||
* new MCP tools → `mcp_tool`
|
|
||||||
* major decisions → `decision`
|
|
||||||
* important bugs → `bug`
|
|
||||||
* config changes → `config`
|
|
||||||
* completed non-trivial tasks → `task_log`
|
|
||||||
|
|
||||||
### RULES
|
|
||||||
|
|
||||||
* One entity = one type
|
|
||||||
* Keep entries short
|
|
||||||
* Do not duplicate
|
|
||||||
* Skip trivial changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SECRETS
|
|
||||||
|
|
||||||
* Never print secrets
|
|
||||||
* Never commit `.env.local`
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
* `.env.local` → runtime
|
|
||||||
* `.env.example` → reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LINKS
|
|
||||||
|
|
||||||
Internal:
|
|
||||||
|
|
||||||
* Relative paths
|
|
||||||
* Spaces → `%20`
|
|
||||||
|
|
||||||
External:
|
|
||||||
|
|
||||||
* Markdown links only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EDITING RULES
|
|
||||||
|
|
||||||
* Do not modify this file unless asked
|
|
||||||
* Do not change structure
|
|
||||||
* Keep instructions short and explicit
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CORE BEHAVIOR
|
|
||||||
|
|
||||||
* Ask first if it improves result quality
|
|
||||||
|
|
||||||
* Otherwise act
|
|
||||||
|
|
||||||
* Always consider tools before solving
|
|
||||||
|
|
||||||
* Prefer tools when useful
|
|
||||||
|
|
||||||
* Minimal changes only
|
|
||||||
|
|
||||||
* Do not invent tools
|
|
||||||
|
|
||||||
* Use tools only if confirmed available
|
|
||||||
|
|
||||||
* Never leak secrets
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" Version="10.4.1" />
|
<PackageReference Include="Microsoft.Extensions.AI" Version="10.4.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
||||||
|
<PackageReference Include="Polly" Version="8.4.2" />
|
||||||
<PackageReference Include="RestSharp" Version="112.0.0" />
|
<PackageReference Include="RestSharp" Version="112.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Polly;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Jira;
|
namespace LazyBear.MCP.Services.Jira;
|
||||||
|
|
||||||
public static class JiraClientFactory
|
public static class JiraClientFactory
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan[] BackoffDurations = {
|
||||||
|
TimeSpan.FromMilliseconds(1000),
|
||||||
|
TimeSpan.FromMilliseconds(2000),
|
||||||
|
TimeSpan.FromMilliseconds(4000)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Policy _retryPolicy = Policy
|
||||||
|
.HandleResult<RestResponse>(response => !response.IsSuccessful && response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||||
|
.Or<RestException>()
|
||||||
|
.WaitAndRetryAsync(
|
||||||
|
retryCount: 3,
|
||||||
|
sleepDurationProvider: attempt => BackoffDurations[attempt],
|
||||||
|
onRetry: (outcome, timespan, attempt, context) => { });
|
||||||
|
|
||||||
public static RestClient CreateClient(IConfiguration configuration)
|
public static RestClient CreateClient(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var jiraUrl = configuration["Jira:Url"] ?? "";
|
var jiraUrl = configuration["Jira:Url"] ?? "";
|
||||||
@@ -20,6 +35,6 @@ public static class JiraClientFactory
|
|||||||
Timeout = TimeSpan.FromMilliseconds(30000)
|
Timeout = TimeSpan.FromMilliseconds(30000)
|
||||||
};
|
};
|
||||||
|
|
||||||
return new RestClient(config);
|
return _retryPolicy.Wrap(new RestClient(config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,16 @@ using ModelContextProtocol.Server;
|
|||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfiguration configuration)
|
public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sConfigTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
||||||
{
|
{
|
||||||
private readonly IKubernetes? _client = clientProvider.Client;
|
private const int MaxSecretKeyLimit = 100;
|
||||||
private readonly string? _clientInitializationError = clientProvider.InitializationError;
|
|
||||||
private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список ConfigMap в namespace")]
|
[McpServerTool, Description("Список ConfigMap в namespace")]
|
||||||
public async Task<string> ListConfigMaps(
|
public async Task<string> ListConfigMaps(
|
||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -53,6 +52,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -82,6 +83,7 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -118,6 +120,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -157,49 +161,4 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
return FormatError("get_secret_keys", ns, ex, name);
|
return FormatError("get_secret_keys", ns, ex, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveNamespace(string? @namespace)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildClientInitializationError()
|
|
||||||
{
|
|
||||||
return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT).";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetClient(out IKubernetes client, out string error)
|
|
||||||
{
|
|
||||||
if (_client is null)
|
|
||||||
{
|
|
||||||
client = null!;
|
|
||||||
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
|
||||||
? string.Empty
|
|
||||||
: $" Детали: {_clientInitializationError}";
|
|
||||||
error = BuildClientInitializationError() + details;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = _client;
|
|
||||||
error = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
|
||||||
{
|
|
||||||
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
|
||||||
|
|
||||||
if (exception is HttpOperationException httpException)
|
|
||||||
{
|
|
||||||
var statusCode = httpException.Response?.StatusCode;
|
|
||||||
var reason = httpException.Response?.ReasonPhrase;
|
|
||||||
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
|
|
||||||
? "-"
|
|
||||||
: httpException.Response!.Content;
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ using ModelContextProtocol.Server;
|
|||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfiguration configuration)
|
public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sDeploymentTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
||||||
{
|
{
|
||||||
private readonly IKubernetes? _client = clientProvider.Client;
|
private const int MinReplicas = 0;
|
||||||
private readonly string? _clientInitializationError = clientProvider.InitializationError;
|
private const int MaxReplicas = 100;
|
||||||
private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список deployment в namespace")]
|
[McpServerTool, Description("Список deployment в namespace")]
|
||||||
public async Task<string> ListDeployments(
|
public async Task<string> ListDeployments(
|
||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -58,6 +58,14 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
|
|
||||||
|
if (replicas < MinReplicas || replicas > MaxReplicas)
|
||||||
|
{
|
||||||
|
return $"Invalid replicas value: {replicas}. Must be between {MinReplicas} and {MaxReplicas}.";
|
||||||
|
}
|
||||||
|
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -87,6 +95,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -116,6 +126,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -151,49 +163,4 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
return FormatError("restart_deployment", ns, ex, name);
|
return FormatError("restart_deployment", ns, ex, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveNamespace(string? @namespace)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildClientInitializationError()
|
|
||||||
{
|
|
||||||
return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT).";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetClient(out IKubernetes client, out string error)
|
|
||||||
{
|
|
||||||
if (_client is null)
|
|
||||||
{
|
|
||||||
client = null!;
|
|
||||||
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
|
||||||
? string.Empty
|
|
||||||
: $" Детали: {_clientInitializationError}";
|
|
||||||
error = BuildClientInitializationError() + details;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = _client;
|
|
||||||
error = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
|
||||||
{
|
|
||||||
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
|
||||||
|
|
||||||
if (exception is HttpOperationException httpException)
|
|
||||||
{
|
|
||||||
var statusCode = httpException.Response?.StatusCode;
|
|
||||||
var reason = httpException.Response?.ReasonPhrase;
|
|
||||||
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
|
|
||||||
? "-"
|
|
||||||
: httpException.Response!.Content;
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ using ModelContextProtocol.Server;
|
|||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfiguration configuration)
|
public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sNetworkTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
||||||
{
|
{
|
||||||
private readonly IKubernetes? _client = clientProvider.Client;
|
|
||||||
private readonly string? _clientInitializationError = clientProvider.InitializationError;
|
|
||||||
private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список service в namespace")]
|
[McpServerTool, Description("Список service в namespace")]
|
||||||
public async Task<string> ListServices(
|
public async Task<string> ListServices(
|
||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -58,6 +55,8 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -91,6 +90,7 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -136,49 +136,4 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
|||||||
return FormatError("list_ingresses", ns, ex);
|
return FormatError("list_ingresses", ns, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveNamespace(string? @namespace)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildClientInitializationError()
|
|
||||||
{
|
|
||||||
return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT).";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetClient(out IKubernetes client, out string error)
|
|
||||||
{
|
|
||||||
if (_client is null)
|
|
||||||
{
|
|
||||||
client = null!;
|
|
||||||
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
|
||||||
? string.Empty
|
|
||||||
: $" Детали: {_clientInitializationError}";
|
|
||||||
error = BuildClientInitializationError() + details;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = _client;
|
|
||||||
error = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
|
||||||
{
|
|
||||||
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
|
||||||
|
|
||||||
if (exception is HttpOperationException httpException)
|
|
||||||
{
|
|
||||||
var statusCode = httpException.Response?.StatusCode;
|
|
||||||
var reason = httpException.Response?.ReasonPhrase;
|
|
||||||
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
|
|
||||||
? "-"
|
|
||||||
: httpException.Response!.Content;
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ using ModelContextProtocol.Server;
|
|||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguration configuration)
|
public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sPodsTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
||||||
{
|
{
|
||||||
private readonly IKubernetes? _client = clientProvider.Client;
|
private const int MaxTailLines = 10;
|
||||||
private readonly string? _clientInitializationError = clientProvider.InitializationError;
|
private const int MinTailLines = 10;
|
||||||
private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список подов в namespace")]
|
[McpServerTool, Description("Список подов в namespace")]
|
||||||
public async Task<string> ListPods(
|
public async Task<string> ListPods(
|
||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -55,6 +55,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -88,6 +90,18 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
int? tailLines = 100,
|
int? tailLines = 100,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateResourceName(name, nameof(name));
|
||||||
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
|
|
||||||
|
if (tailLines < MinTailLines)
|
||||||
|
{
|
||||||
|
tailLines = MinTailLines;
|
||||||
|
}
|
||||||
|
if (tailLines > MaxTailLines)
|
||||||
|
{
|
||||||
|
tailLines = MaxTailLines;
|
||||||
|
}
|
||||||
|
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
{
|
{
|
||||||
@@ -100,7 +114,7 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
name,
|
name,
|
||||||
ns,
|
ns,
|
||||||
container: container,
|
container: container,
|
||||||
tailLines: tailLines,
|
tailLines: (int?)tailLines,
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
using var reader = new StreamReader(logStream);
|
using var reader = new StreamReader(logStream);
|
||||||
@@ -118,49 +132,4 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
return FormatError("get_pod_logs", ns, ex, name);
|
return FormatError("get_pod_logs", ns, ex, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveNamespace(string? @namespace)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildClientInitializationError()
|
|
||||||
{
|
|
||||||
return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT).";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetClient(out IKubernetes client, out string error)
|
|
||||||
{
|
|
||||||
if (_client is null)
|
|
||||||
{
|
|
||||||
client = null!;
|
|
||||||
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
|
||||||
? string.Empty
|
|
||||||
: $" Детали: {_clientInitializationError}";
|
|
||||||
error = BuildClientInitializationError() + details;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
client = _client;
|
|
||||||
error = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
|
||||||
{
|
|
||||||
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
|
||||||
|
|
||||||
if (exception is HttpOperationException httpException)
|
|
||||||
{
|
|
||||||
var statusCode = httpException.Response?.StatusCode;
|
|
||||||
var reason = httpException.Response?.ReasonPhrase;
|
|
||||||
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
|
|
||||||
? "-"
|
|
||||||
: httpException.Response!.Content;
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
107
LazyBear.MCP/Services/Kubernetes/KubernetesToolsBase.cs
Normal file
107
LazyBear.MCP/Services/Kubernetes/KubernetesToolsBase.cs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using k8s;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
|
[McpServerToolType]
|
||||||
|
public abstract class KubernetesToolsBase(K8sClientProvider clientProvider, IConfiguration configuration, ILogger? logger = null)
|
||||||
|
{
|
||||||
|
protected readonly IKubernetes? _client = clientProvider.Client;
|
||||||
|
protected readonly string? _clientInitializationError = clientProvider.InitializationError;
|
||||||
|
protected readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
||||||
|
protected readonly ILogger? _logger = logger;
|
||||||
|
|
||||||
|
protected static readonly Regex NamespaceRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||||
|
protected static readonly Regex ResourceNameRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
protected string ResolveNamespace(string? @namespace)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ValidateNamespace(string value, string parameterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Namespace не может быть пустым", parameterName);
|
||||||
|
}
|
||||||
|
if (value.Length > 63)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Namespace не может быть длиннее 63 символов", parameterName);
|
||||||
|
}
|
||||||
|
if (!NamespaceRegex.IsMatch(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Невалидный формат namespace: '{value}'. Только lowercase alphanumetic, -, не может начинаться/заканчиваться на -", parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ValidateResourceName(string value, string parameterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Resource name не может быть пустым", parameterName);
|
||||||
|
}
|
||||||
|
if (value.Length > 253)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Resource name не может быть длиннее 253 символов", parameterName);
|
||||||
|
}
|
||||||
|
if (!ResourceNameRegex.IsMatch(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Невалидный формат: '{value}'. Только lowercase alphanumetic, -, не может начинаться/заканчиваться на -", parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool TryGetClient(out IKubernetes client, out string error)
|
||||||
|
{
|
||||||
|
if (_client is null)
|
||||||
|
{
|
||||||
|
client = null!;
|
||||||
|
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
||||||
|
? string.Empty
|
||||||
|
: $" Детали: {_clientInitializationError}";
|
||||||
|
error = "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT)." + details;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
client = _client;
|
||||||
|
error = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
||||||
|
{
|
||||||
|
_logger?.LogError(exception, "Ошибка Kubernetes в tool '{ToolName}' (namespace='{Namespace}'{ResourcePart}): {ExceptionMessage}",
|
||||||
|
toolName, @namespace,
|
||||||
|
string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'",
|
||||||
|
exception.Message);
|
||||||
|
|
||||||
|
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
||||||
|
|
||||||
|
if (exception is global::k8s.Autorest.HttpOperationException httpException)
|
||||||
|
{
|
||||||
|
var statusCode = httpException.Response?.StatusCode;
|
||||||
|
var reason = httpException.Response?.ReasonPhrase;
|
||||||
|
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
|
||||||
|
? "-"
|
||||||
|
: httpException.Response!.Content;
|
||||||
|
|
||||||
|
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string FormatException(string toolName, Exception exception, string? resource = null)
|
||||||
|
{
|
||||||
|
_logger?.LogError(exception, "Ошибка exception в tool '{ToolName}'{ResourcePart}: {ExceptionMessage}",
|
||||||
|
toolName,
|
||||||
|
string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'",
|
||||||
|
exception.Message);
|
||||||
|
|
||||||
|
var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'";
|
||||||
|
return $"Ошибка в tool '{toolName}'{resourcePart}: {exception.GetType().Name}: {exception.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
245
__DONT_USE.md
Normal file
245
__DONT_USE.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
## AGENTS.md
|
||||||
|
|
||||||
|
### PRIORITY
|
||||||
|
|
||||||
|
1. User request
|
||||||
|
2. This file
|
||||||
|
3. Existing code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CODE
|
||||||
|
|
||||||
|
### STACK
|
||||||
|
|
||||||
|
* .NET / C#
|
||||||
|
* ASP.NET Core
|
||||||
|
* MCP
|
||||||
|
|
||||||
|
### STRUCTURE
|
||||||
|
|
||||||
|
* `Server/` — endpoints
|
||||||
|
* `Services/` — business logic
|
||||||
|
* `Tools/` — MCP tools
|
||||||
|
|
||||||
|
### RULES
|
||||||
|
|
||||||
|
**Before edit**
|
||||||
|
|
||||||
|
* Read related code
|
||||||
|
* Reuse existing patterns
|
||||||
|
* Do not over-engineer
|
||||||
|
|
||||||
|
**After edit**
|
||||||
|
|
||||||
|
* Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
* Build must succeed
|
||||||
|
* Do not break MCP protocol
|
||||||
|
* Keep diff minimal
|
||||||
|
|
||||||
|
**Style**
|
||||||
|
|
||||||
|
* Match existing style
|
||||||
|
* Avoid duplication
|
||||||
|
* Prefer small changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## COMMUNICATION
|
||||||
|
|
||||||
|
### LANGUAGE
|
||||||
|
|
||||||
|
* Output: Russian
|
||||||
|
* Code: English
|
||||||
|
* Comments/commits: Russian
|
||||||
|
|
||||||
|
### BEHAVIOR
|
||||||
|
|
||||||
|
* Be concise
|
||||||
|
* Do not explain obvious things
|
||||||
|
* Do not produce long texts
|
||||||
|
* Prefer action when no clarification is needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### QUESTIONS
|
||||||
|
|
||||||
|
Default:
|
||||||
|
|
||||||
|
* If result can be improved by user choice → ASK FIRST
|
||||||
|
* Do not execute immediately if preferences affect result
|
||||||
|
|
||||||
|
Ask BEFORE action if:
|
||||||
|
|
||||||
|
* Multiple valid directions exist
|
||||||
|
* Result depends on user preference
|
||||||
|
* Request is broad (e.g. "suggest", "recommend", "generate")
|
||||||
|
|
||||||
|
Otherwise:
|
||||||
|
|
||||||
|
* Proceed with best reasonable assumption
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### QUESTION TOOL
|
||||||
|
|
||||||
|
`question` is the UI tool for user choices in OpenCode.
|
||||||
|
|
||||||
|
Use `question` BEFORE answering when:
|
||||||
|
|
||||||
|
* 2+ meaningful options exist
|
||||||
|
* clarification improves result quality
|
||||||
|
* choice affects architecture, config, data, or output
|
||||||
|
|
||||||
|
Do NOT skip `question` in these cases.
|
||||||
|
|
||||||
|
Do NOT use when:
|
||||||
|
|
||||||
|
* request is already specific
|
||||||
|
* only one valid answer exists
|
||||||
|
* clarification does not change result
|
||||||
|
|
||||||
|
If unavailable:
|
||||||
|
|
||||||
|
* ask in plain text
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RESTRICTIONS
|
||||||
|
|
||||||
|
* Do not end with only a question
|
||||||
|
* Do not expose secrets
|
||||||
|
* Do not repeat user text
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TOOLS
|
||||||
|
|
||||||
|
Always assume tools MAY be available.
|
||||||
|
|
||||||
|
Before solving:
|
||||||
|
|
||||||
|
* Identify relevant tools
|
||||||
|
* Prefer tools when they simplify the task
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Do not invent tools
|
||||||
|
* Use only confirmed available tools
|
||||||
|
* If availability unclear → proceed without them
|
||||||
|
|
||||||
|
Tools are part of the solution, not optional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MEMORY
|
||||||
|
|
||||||
|
Use ONLY if memory tools are available.
|
||||||
|
|
||||||
|
### READ FIRST
|
||||||
|
|
||||||
|
Before coding or assumptions:
|
||||||
|
|
||||||
|
1. Try `read_graph`
|
||||||
|
2. Then `search_nodes()`
|
||||||
|
3. Avoid duplicate observations
|
||||||
|
|
||||||
|
If unavailable:
|
||||||
|
|
||||||
|
* Skip memory usage
|
||||||
|
|
||||||
|
### KEY FORMAT
|
||||||
|
|
||||||
|
```text
|
||||||
|
lazybear/<type>/<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
lazybear/bug/auth-fail
|
||||||
|
lazybear/decision/mcp-timeout
|
||||||
|
lazybear/config/jira-base-url
|
||||||
|
```
|
||||||
|
|
||||||
|
### ALLOWED TYPES
|
||||||
|
|
||||||
|
* `architecture`
|
||||||
|
* `mcp_tool`
|
||||||
|
* `decision`
|
||||||
|
* `bug`
|
||||||
|
* `config`
|
||||||
|
* `task_log`
|
||||||
|
|
||||||
|
### WRITE ONLY WHEN USEFUL
|
||||||
|
|
||||||
|
* architecture changes → `architecture`
|
||||||
|
* new MCP tools → `mcp_tool`
|
||||||
|
* major decisions → `decision`
|
||||||
|
* important bugs → `bug`
|
||||||
|
* config changes → `config`
|
||||||
|
* completed non-trivial tasks → `task_log`
|
||||||
|
|
||||||
|
### RULES
|
||||||
|
|
||||||
|
* One entity = one type
|
||||||
|
* Keep entries short
|
||||||
|
* Do not duplicate
|
||||||
|
* Skip trivial changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SECRETS
|
||||||
|
|
||||||
|
* Never print secrets
|
||||||
|
* Never commit `.env.local`
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
* `.env.local` → runtime
|
||||||
|
* `.env.example` → reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LINKS
|
||||||
|
|
||||||
|
Internal:
|
||||||
|
|
||||||
|
* Relative paths
|
||||||
|
* Spaces → `%20`
|
||||||
|
|
||||||
|
External:
|
||||||
|
|
||||||
|
* Markdown links only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EDITING RULES
|
||||||
|
|
||||||
|
* Do not modify this file unless asked
|
||||||
|
* Do not change structure
|
||||||
|
* Keep instructions short and explicit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CORE BEHAVIOR
|
||||||
|
|
||||||
|
* Ask first if it improves result quality
|
||||||
|
|
||||||
|
* Otherwise act
|
||||||
|
|
||||||
|
* Always consider tools before solving
|
||||||
|
|
||||||
|
* Prefer tools when useful
|
||||||
|
|
||||||
|
* Minimal changes only
|
||||||
|
|
||||||
|
* Do not invent tools
|
||||||
|
|
||||||
|
* Use tools only if confirmed available
|
||||||
|
|
||||||
|
* Never leak secrets
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"model": "ollama/qwen3.5-agent"
|
"model": "ollama/qwen3.5-agent",
|
||||||
|
"instructions": [ "AGENTS.md" ]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user