节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
5.23 KiB
Stardust
using System.Text.Json;

namespace Stardust.Mcp;

/// <summary>操作风险级别。写入工具元数据,由引擎在调用前强制执行(设计 §4.2)</summary>
public enum McpRiskLevel
{
    /// <summary>只读,无副作用。需要 scope 域:read</summary>
    L0,

    /// <summary>可逆写,平台内可回滚。需要 scope 域:write,审计 fail-closed</summary>
    L1,

    /// <summary>高危,不可逆或影响运行时。需要 scope 域:admin,且参数必须显式 confirm:true</summary>
    L2,
}

/// <summary>MCP工具契约。实现类由引擎反射扫描注册,直出 tools/list(废弃旧版二段式 invoke_action)</summary>
public interface IMcpTool
{
    /// <summary>工具名(stardust_域_动作,snake_case)</summary>
    String Name { get; }

    /// <summary>描述(英文,Q4 决策:利于模型选工具)</summary>
    String Description { get; }

    /// <summary>风险级别</summary>
    McpRiskLevel Risk { get; }

    /// <summary>scope域(apps/nodes/config/deploy/monitor/now/platform)</summary>
    String ScopeDomain { get; }

    /// <summary>是否要求scope。false=任何有效令牌可调用(如自检工具)</summary>
    Boolean RequireScope { get; }

    /// <summary>输入参数JSON Schema</summary>
    JsonElement InputSchema { get; }

    /// <summary>执行工具。返回对象将被序列化进 content[0].text</summary>
    /// <param name="args">输入参数</param>
    /// <param name="ctx">调用上下文</param>
    Task<Object> InvokeAsync(JsonElement args, McpContext ctx);
}

/// <summary>MCP工具基类。提供通用实现,子类重写元数据与 InvokeAsync</summary>
public abstract class McpToolBase : IMcpTool
{
    public abstract String Name { get; }

    public abstract String Description { get; }

    public abstract McpRiskLevel Risk { get; }

    public abstract String ScopeDomain { get; }

    /// <summary>默认要求scope;自检类工具重写为 false</summary>
    public virtual Boolean RequireScope => true;

    /// <summary>输入参数JSON Schema。默认空对象</summary>
    public virtual JsonElement InputSchema => default;

    public abstract Task<Object> InvokeAsync(JsonElement args, McpContext ctx);

    public static Int32 ReadInt(JsonElement element, String name) => GetInt32(element, name);

    public static String ReadString(JsonElement element, String name) => GetString(element, name);

    protected static String GetString(JsonElement element, String name)
        => element.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;

    protected static Int32 GetInt32(JsonElement element, String name)
        => element.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt32() : 0;

    protected static Int64 GetInt64(JsonElement element, String name)
        => element.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt64() : 0;

    protected static Boolean GetBoolean(JsonElement element, String name)
        => element.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;

    protected static Int32 Page(JsonElement args)
    {
        var page = GetInt32(args, "page");
        return page <= 0 ? 1 : page;
    }

    protected static Int32 PageSize(JsonElement args)
    {
        var size = GetInt32(args, "page_size");
        if (size <= 0) size = 20;
        return size > 100 ? 100 : size;
    }

    protected static DateTime Since(JsonElement args, Int32 defaultMinutes = 1440)
    {
        var raw = GetString(args, "since");
        if (String.IsNullOrEmpty(raw)) return DateTime.Now.AddMinutes(-defaultMinutes);
        if (raw.StartsWith("-") && raw.EndsWith("m", StringComparison.OrdinalIgnoreCase) &&
            Int32.TryParse(raw[1..^1], out var minutes))
            return DateTime.Now.AddMinutes(-minutes);
        return DateTime.TryParse(raw, out var dt) ? dt : DateTime.Now.AddMinutes(-defaultMinutes);
    }

    protected void Need(JsonElement args, String name)
    {
        if (!args.TryGetProperty(name, out var v) || v.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
            throw new McpException(-32602, $"无效参数:缺少 {name}");
    }
}

/// <summary>MCP调用上下文。一次会话的身份、授权与来源信息</summary>
public sealed class McpContext
{
    /// <summary>令牌编号</summary>
    public Int32 TokenId { get; set; }

    /// <summary>令牌名称(审计快照)</summary>
    public String TokenName { get; set; }

    /// <summary>绑定用户Id。0=未绑定</summary>
    public Int32 UserId { get; set; }

    /// <summary>绑定项目Id。0=未绑定</summary>
    public Int32 ProjectId { get; set; }

    /// <summary>授权范围集合(域:级别,忽略大小写)</summary>
    public HashSet<String> Scopes { get; set; } = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>调用方IP</summary>
    public String CallerIp { get; set; }

    /// <summary>调用方User-Agent</summary>
    public String UserAgent { get; set; }

    /// <summary>链路追踪Id</summary>
    public String TraceId { get; set; }

    /// <summary>宿主服务容器。工具可按需解析宿主注册的业务服务</summary>
    public IServiceProvider Services { get; set; }
}