using System.Text.Json;
using NewLife;
using NewLife.Data;
using Stardust.Data;
using Stardust.Data.Nodes;
using Stardust.Models;
namespace Stardust.Mcp.Tools.Nodes;
public class NodeListTool : McpToolBase
{
public override String Name => "stardust_node_list";
public override String Description => "List nodes in the token project. Optional keyword and online filter.";
public override McpRiskLevel Risk => McpRiskLevel.L0;
public override String ScopeDomain => "nodes";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
keyword = new { type = "string", description = "名称/IP/编码关键字" },
online_only = new { type = "boolean", description = "只看在线" },
page_size = new { type = "integer", description = "每页条数" },
},
});
public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
McpGuard.NeedProject(ctx);
var key = GetString(args, "keyword");
var pageIndex = Page(args);
var pageSize = PageSize(args);
var page = new PageParameter
{
PageIndex = pageIndex,
PageSize = pageSize,
RetrieveTotalCount = true,
};
// 只看在线走节点在线页的 NodeOnline.Search;其余走节点管理页的 Node.Search。分页都在查询内完成。
if (GetBoolean(args, "online_only"))
{
var onlines = NodeOnline.Search(ctx.ProjectId, -1, -1, -1, null, default, default, key, page);
var rows = onlines.Select(e => new { id = e.NodeID, e.Name, e.IP, e.Version, e.Category, online = true }).ToArray();
var total = (Int32)page.TotalCount;
return Task.FromResult<Object>(McpResult.Ok($"节点 {rows.Length}/{total} 台", new { records = rows },
null, null, McpResult.Paged(total, pageIndex, pageSize)));
}
var list = Node.Search(ctx.ProjectId, -1, -1, null, null, (OSKinds)(-1), null, null, null, null, null, default, default, key, page);
var records = list.Select(e => new
{
id = e.ID,
e.Name,
e.IP,
e.Version,
e.Category,
online = NodeOnline.FindByNodeId(e.ID) != null,
}).ToArray();
var count = (Int32)page.TotalCount;
return Task.FromResult<Object>(McpResult.Ok($"节点 {records.Length}/{count} 台", new { records },
null, null, McpResult.Paged(count, pageIndex, pageSize)));
}
}
public class NodeDetailTool : McpToolBase
{
public override String Name => "stardust_node_detail";
public override String Description => "Read one node: online state, version, resource watermark and recent commands.";
public override McpRiskLevel Risk => McpRiskLevel.L0;
public override String ScopeDomain => "nodes";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
node_id = new { type = "integer", description = "节点编号" },
},
required = new[] { "node_id" },
});
public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
var node = McpGuard.Node(ctx, GetInt32(args, "node_id"));
var online = NodeOnline.FindAllByNodeId(node.ID).OrderByDescending(e => e.UpdateTime).FirstOrDefault();
var commands = NodeCommand.Search(node.ID, null, default, default, null, new PageParameter
{
PageIndex = 1,
PageSize = 10,
Sort = NodeCommand._.Id,
Desc = true,
}).Select(e => new { e.Command, e.Result, e.StartTime }).ToArray();
return Task.FromResult<Object>(McpResult.Ok($"{node.Name} {(online == null ? "离线" : "在线")}", new
{
id = node.ID,
node.Name,
node.IP,
node.Version,
cpu = online?.CpuRate,
memory = online?.MemoryUsed,
commands,
}));
}
}
public class NodeAppsTool : McpToolBase
{
public override String Name => "stardust_node_apps";
public override String Description => "List application instances running on one node.";
public override McpRiskLevel Risk => McpRiskLevel.L0;
public override String ScopeDomain => "nodes";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
node_id = new { type = "integer", description = "节点编号" },
},
required = new[] { "node_id" },
});
public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
var node = McpGuard.Node(ctx, GetInt32(args, "node_id"));
// PageSize 为 0 表示不分页,返回该节点上的全部实例,与应用在线页同一查询。
var rows = AppOnline.Search(ctx.ProjectId, -1, node.ID, null, default, default, null, new PageParameter { PageSize = 0 })
.Select(e => new { e.AppId, e.Name, e.Version, e.ProcessId, e.IP }).ToArray();
return Task.FromResult<Object>(McpResult.Ok($"{node.Name} 上 {rows.Length} 个应用实例", new { records = rows }));
}
}
public class NodeCommandTool : McpToolBase
{
public override String Name => "stardust_node_command";
public override String Description => "Execute a command on one node. Dangerous commands and any non-readonly command require confirm=true. The operator is the MCP token.";
public override McpRiskLevel Risk => McpRiskLevel.L2;
public override String ScopeDomain => "nodes";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
node_id = new { type = "integer", description = "节点编号" },
command = new { type = "string", description = "要执行的命令" },
args = new { type = "string", description = "命令附加参数" },
confirm = new { type = "boolean", description = "危险命令或非只读命令必须传 true" },
},
required = new[] { "node_id", "command" },
});
public override async Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
var node = McpGuard.Node(ctx, GetInt32(args, "node_id"));
var command = GetString(args, "command");
if (command.IsNullOrEmpty()) throw new McpException(-32602, "无效参数:缺少 command。");
if (Danger(command) && !GetBoolean(args, "confirm"))
throw new McpException(-32003, "该命令命中危险规则,必须传 confirm=true。");
var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
var reply = await ops.SendNodeCommandAsync(node.Code, command, GetString(args, "args"));
return McpResult.Ok($"已向节点 {node.Name} 下发命令 {command}", new { node_id = node.ID, command, reply },
null, "命令已在节点上执行,无法自动回滚;如需恢复请手动执行反向命令");
}
private static Boolean Danger(String command)
{
var c = command.ToLowerInvariant();
return c.Contains("rm ") || c.Contains("mkfs") || c.Contains("dd ") || c.Contains("shutdown") || c.Contains("reboot");
}
}
public class AppCommandTool : McpToolBase
{
public override String Name => "stardust_app_command";
public override String Description => "Send a command to one application instance. Requires confirm=true.";
public override McpRiskLevel Risk => McpRiskLevel.L2;
public override String ScopeDomain => "apps";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
app_id = new { type = "integer", description = "应用编号" },
command = new { type = "string", description = "要执行的命令" },
client_id = new { type = "string", description = "客户端实例Id,可空" },
args = new { type = "string", description = "命令附加参数" },
confirm = new { type = "boolean", description = "高危操作必须传 true" },
},
required = new[] { "app_id", "command" },
});
public override async Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
var app = McpGuard.App(ctx, GetInt32(args, "app_id"));
var command = GetString(args, "command");
if (command.IsNullOrEmpty()) throw new McpException(-32602, "无效参数:缺少 command。");
var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
var reply = await ops.SendAppCommandAsync(app.Name, GetString(args, "client_id"), command, GetString(args, "args"));
return McpResult.Ok($"已向应用 {app.Name} 下发命令 {command}", new { app_id = app.Id, command, reply },
null, "命令已在应用实例上执行,无法自动回滚;如需恢复请手动执行反向命令");
}
}
public class AppProcessTool : McpToolBase
{
public override String Name => "stardust_app_process";
public override String Description => "Start, restart or stop an application process. stop requires confirm=true.";
public override McpRiskLevel Risk => McpRiskLevel.L1;
public override String ScopeDomain => "apps";
public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
app_id = new { type = "integer", description = "应用编号" },
action = new { type = "string", description = "操作类型", @enum = new[] { "start", "restart", "stop" } },
args = new { type = "string", description = "命令附加参数" },
confirm = new { type = "boolean", description = "stop 必须传 true" },
},
required = new[] { "app_id", "action" },
});
public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
{
var action = GetString(args, "action");
if (!new[] { "start", "restart", "stop" }.Contains(action)) throw new McpException(-32602, "action 只能是 start/restart/stop。");
if (action == "stop" && !GetBoolean(args, "confirm")) throw new McpException(-32003, "stop 必须传 confirm=true。");
args = JsonSerializer.SerializeToElement(new { app_id = GetInt32(args, "app_id"), command = "app/" + action, args = GetString(args, "args"), confirm = true });
return new AppCommandTool().InvokeAsync(args, ctx);
}
}
|