@spaceflow/core 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/core",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Spaceflow 核心能力库",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -81,4 +81,24 @@ export class ExtensionLoader {
81
81
  }
82
82
  return result;
83
83
  }
84
+
85
+ /**
86
+ * 获取所有 MCP 资源
87
+ * 返回扩展中定义的 resources 字段
88
+ */
89
+ getResources(): Array<{
90
+ extensionName: string;
91
+ resources: NonNullable<ExtensionDefinition["resources"]>;
92
+ }> {
93
+ const result: Array<{
94
+ extensionName: string;
95
+ resources: NonNullable<ExtensionDefinition["resources"]>;
96
+ }> = [];
97
+ for (const ext of this.extensions.values()) {
98
+ if (ext.resources && ext.resources.length > 0) {
99
+ result.push({ extensionName: ext.name, resources: ext.resources });
100
+ }
101
+ }
102
+ return result;
103
+ }
84
104
  }
@@ -1,12 +1,19 @@
1
1
  import { t } from "@spaceflow/core";
2
- import type { VerboseLevel } from "@spaceflow/core";
2
+ import type { VerboseLevel, McpResourceDefinition, SpaceflowContext } from "@spaceflow/core";
3
3
  import { shouldLog, type McpToolMetadata } from "@spaceflow/core";
4
+ import { readConfigSync } from "@spaceflow/shared";
4
5
  import type { ExtensionLoader } from "../../cli-runtime/extension-loader";
5
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
8
  import { z } from "zod";
8
9
  import { spawn } from "child_process";
9
10
 
11
+ /** 内部 resource 收集结构 */
12
+ interface CollectedResource {
13
+ resource: McpResourceDefinition;
14
+ ctx: SpaceflowContext;
15
+ }
16
+
10
17
  export class McpService {
11
18
  constructor(private readonly extensionLoader: ExtensionLoader) {}
12
19
 
@@ -57,16 +64,27 @@ export class McpService {
57
64
  }
58
65
  }
59
66
 
60
- if (allTools.length === 0) {
67
+ // 收集扩展 resources + 内置 resources
68
+ const allResources = this.collectResources(verbose);
69
+
70
+ if (allTools.length === 0 && allResources.length === 0) {
61
71
  console.error(t("mcp:noToolsFound"));
62
72
  console.error(t("mcp:noToolsHint"));
63
73
  process.exit(1);
64
74
  }
65
75
 
66
76
  if (shouldLog(verbose, 1)) {
67
- console.error(t("mcp:toolsFound", { count: allTools.length }));
68
- for (const { tool } of allTools) {
69
- console.error(` - ${tool.name}`);
77
+ if (allTools.length > 0) {
78
+ console.error(t("mcp:toolsFound", { count: allTools.length }));
79
+ for (const { tool } of allTools) {
80
+ console.error(` - ${tool.name}`);
81
+ }
82
+ }
83
+ if (allResources.length > 0) {
84
+ console.error(t("mcp:resourcesFound", { count: allResources.length }));
85
+ for (const { resource } of allResources) {
86
+ console.error(` - ${resource.name} (${resource.uri})`);
87
+ }
70
88
  }
71
89
  }
72
90
 
@@ -78,7 +96,93 @@ export class McpService {
78
96
  }
79
97
 
80
98
  // 被 MCP 客户端通过管道调用,正常启动 stdio server
81
- await this.runServer(allTools, verbose);
99
+ await this.runServer(allTools, allResources, verbose);
100
+ }
101
+
102
+ /**
103
+ * 收集所有 MCP 资源(扩展 + 内置)
104
+ */
105
+ private collectResources(verbose?: VerboseLevel): CollectedResource[] {
106
+ const ctx = this.extensionLoader.getContext();
107
+ const allResources: CollectedResource[] = [];
108
+
109
+ // 1. 收集扩展声明的 resources
110
+ const extensionResources = this.extensionLoader.getResources();
111
+ for (const { extensionName, resources } of extensionResources) {
112
+ if (shouldLog(verbose, 2)) {
113
+ console.error(` 扩展 ${extensionName} 提供 ${resources.length} 个 MCP 资源`);
114
+ }
115
+ for (const resource of resources) {
116
+ allResources.push({ resource, ctx });
117
+ }
118
+ }
119
+
120
+ // 2. 内置资源:项目配置(过滤敏感字段)
121
+ allResources.push({
122
+ resource: {
123
+ name: "spaceflow-config",
124
+ uri: "spaceflow://config",
125
+ title: "Spaceflow Configuration",
126
+ description: "当前项目的 Spaceflow 配置(已过滤敏感字段)",
127
+ mimeType: "application/json",
128
+ handler: async (_uri, ctx) => {
129
+ const config = readConfigSync(ctx.cwd);
130
+ return JSON.stringify(this.sanitizeConfig(config), null, 2);
131
+ },
132
+ },
133
+ ctx,
134
+ });
135
+
136
+ // 3. 内置资源:扩展列表
137
+ allResources.push({
138
+ resource: {
139
+ name: "spaceflow-extensions",
140
+ uri: "spaceflow://extensions",
141
+ title: "Installed Extensions",
142
+ description: "当前项目已安装的 Spaceflow 扩展及其工具/资源",
143
+ mimeType: "application/json",
144
+ handler: async () => {
145
+ const extensions = this.extensionLoader.getExtensions();
146
+ const summary = extensions.map((ext) => ({
147
+ name: ext.name,
148
+ version: ext.version,
149
+ description: ext.description,
150
+ commands: ext.commands.map((c) => c.name),
151
+ tools: (ext.tools || []).map((t) => ({ name: t.name, description: t.description })),
152
+ resources: (ext.resources || []).map((r) => ({
153
+ name: r.name,
154
+ uri: r.uri,
155
+ description: r.description,
156
+ })),
157
+ }));
158
+ return JSON.stringify(summary, null, 2);
159
+ },
160
+ },
161
+ ctx,
162
+ });
163
+
164
+ return allResources;
165
+ }
166
+
167
+ /**
168
+ * 过滤配置中的敏感字段
169
+ */
170
+ private sanitizeConfig(config: Record<string, any>): Record<string, any> {
171
+ const sensitiveKeys = ["token", "apiKey", "appSecret", "authToken", "apikey", "secret"];
172
+ const sanitize = (obj: any): any => {
173
+ if (obj === null || obj === undefined || typeof obj !== "object") return obj;
174
+ if (Array.isArray(obj)) return obj.map(sanitize);
175
+ const result: Record<string, any> = {};
176
+ for (const [key, value] of Object.entries(obj)) {
177
+ if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk.toLowerCase()))) {
178
+ result[key] = value ? "***" : "";
179
+ } else {
180
+ result[key] = sanitize(value);
181
+ }
182
+ }
183
+ return result;
184
+ };
185
+ return sanitize(config);
82
186
  }
83
187
 
84
188
  /**
@@ -86,6 +190,7 @@ export class McpService {
86
190
  */
87
191
  private async runServer(
88
192
  allTools: Array<{ tool: McpToolMetadata; handler: any; ctx: any }>,
193
+ allResources: CollectedResource[],
89
194
  verbose?: VerboseLevel,
90
195
  ): Promise<void> {
91
196
  const server = new McpServer({ name: "spaceflow", version: "1.0.0" });
@@ -126,6 +231,43 @@ export class McpService {
126
231
  );
127
232
  }
128
233
 
234
+ // 注册所有资源(使用 v2 API: server.registerResource)
235
+ for (const { resource, ctx } of allResources) {
236
+ server.registerResource(
237
+ resource.name,
238
+ resource.uri,
239
+ {
240
+ title: resource.title,
241
+ description: resource.description,
242
+ mimeType: resource.mimeType || "application/json",
243
+ },
244
+ async (uri) => {
245
+ try {
246
+ const text = await resource.handler(uri.href, ctx);
247
+ return {
248
+ contents: [
249
+ {
250
+ uri: uri.href,
251
+ mimeType: resource.mimeType || "application/json",
252
+ text,
253
+ },
254
+ ],
255
+ };
256
+ } catch (error) {
257
+ return {
258
+ contents: [
259
+ {
260
+ uri: uri.href,
261
+ mimeType: "text/plain",
262
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
263
+ },
264
+ ],
265
+ };
266
+ }
267
+ },
268
+ );
269
+ }
270
+
129
271
  // 启动 stdio 传输
130
272
  const transport = new StdioServerTransport();
131
273
  await server.connect(transport);
@@ -136,6 +136,29 @@ export interface ServiceDefinition {
136
136
  factory: (ctx: SpaceflowContext) => unknown;
137
137
  }
138
138
 
139
+ /**
140
+ * MCP 资源定义
141
+ */
142
+ export interface McpResourceDefinition {
143
+ /** 资源名称(唯一标识符) */
144
+ name: string;
145
+ /** 资源 URI(固定 URI,如 "config://spaceflow") */
146
+ uri: string;
147
+ /** 资源标题(人类可读) */
148
+ title?: string;
149
+ /** 资源描述 */
150
+ description?: string;
151
+ /** MIME 类型(默认 application/json) */
152
+ mimeType?: string;
153
+ /**
154
+ * 资源读取处理函数
155
+ * @param uri 请求的 URI
156
+ * @param ctx Spaceflow 上下文
157
+ * @returns 资源内容(字符串)
158
+ */
159
+ handler: (uri: string, ctx: SpaceflowContext) => Promise<string>;
160
+ }
161
+
139
162
  /**
140
163
  * MCP 工具定义
141
164
  */
@@ -174,6 +197,8 @@ export interface ExtensionDefinition {
174
197
  commands: CommandDefinition[];
175
198
  /** MCP 工具列表 */
176
199
  tools?: McpToolDefinition[];
200
+ /** MCP 资源列表 */
201
+ resources?: McpResourceDefinition[];
177
202
  /** 服务定义列表 */
178
203
  services?: ServiceDefinition[];
179
204
  /**
@@ -16,6 +16,7 @@
16
16
  "noToolsFound": "❌ No MCP tools found",
17
17
  "noToolsHint": " Hint: Make sure you have installed MCP-enabled extensions that export mcpService or getMcpTools\n If the working directory is incorrect, set the environment variable in your MCP config:\n \"env\": { \"SPACEFLOW_CWD\": \"/path/to/your/project\" }",
18
18
  "toolsFound": "✅ Found {{count}} MCP tools",
19
+ "resourcesFound": "📦 Found {{count}} MCP resources",
19
20
  "ttyHint": "💡 MCP Server must be started by an MCP client (e.g. Cursor, Claude Desktop) via pipe.\n Add the following config to your MCP client:\n\n \"spaceflow\": {\n \"command\": \"npx\",\n \"args\": [\"@spaceflow/cli\", \"mcp\"],\n \"env\": { \"SPACEFLOW_CWD\": \"/path/to/your/project\" }\n }",
20
21
  "serverStarted": "🚀 MCP Server started with {{count}} tools"
21
22
  }
@@ -16,6 +16,7 @@
16
16
  "noToolsFound": "❌ 没有找到任何 MCP 工具",
17
17
  "noToolsHint": " 提示: 确保已安装支持 MCP 的扩展,并导出 mcpService 或 getMcpTools\n 如果工作目录不正确,请在 MCP 配置中设置环境变量:\n \"env\": { \"SPACEFLOW_CWD\": \"/path/to/your/project\" }",
18
18
  "toolsFound": "✅ 共发现 {{count}} 个 MCP 工具",
19
+ "resourcesFound": "📦 共发现 {{count}} 个 MCP 资源",
19
20
  "ttyHint": "💡 MCP Server 需要由 MCP 客户端(如 Cursor、Claude Desktop)通过管道启动。\n 请将以下配置添加到你的 MCP 客户端中:\n\n \"spaceflow\": {\n \"command\": \"npx\",\n \"args\": [\"@spaceflow/cli\", \"mcp\"],\n \"env\": { \"SPACEFLOW_CWD\": \"/path/to/your/project\" }\n }",
20
21
  "serverStarted": "🚀 MCP Server 已启动,共 {{count}} 个工具"
21
22
  }
@@ -383,11 +383,11 @@ describe("GiteaAdapter", () => {
383
383
  });
384
384
 
385
385
  describe("listPullReviews", () => {
386
- it("应请求正确的 URL", async () => {
386
+ it("应请求正确的 URL(带分页参数)", async () => {
387
387
  fetchSpy.mockResolvedValue(mockResponse([]));
388
388
  await adapter.listPullReviews("owner", "repo", 42);
389
389
  expect(fetchSpy).toHaveBeenCalledWith(
390
- "https://gitea.example.com/api/v1/repos/owner/repo/pulls/42/reviews",
390
+ "https://gitea.example.com/api/v1/repos/owner/repo/pulls/42/reviews?page=1&limit=50",
391
391
  expect.anything(),
392
392
  );
393
393
  });
@@ -438,7 +438,20 @@ export class GiteaAdapter implements GitProvider {
438
438
  }
439
439
 
440
440
  async listPullReviews(owner: string, repo: string, index: number): Promise<PullReview[]> {
441
- return this.request<PullReview[]>("GET", `/repos/${owner}/${repo}/pulls/${index}/reviews`);
441
+ const allReviews: PullReview[] = [];
442
+ let page = 1;
443
+ const limit = 50;
444
+ while (true) {
445
+ const reviews = await this.request<PullReview[]>(
446
+ "GET",
447
+ `/repos/${owner}/${repo}/pulls/${index}/reviews?page=${page}&limit=${limit}`,
448
+ );
449
+ if (!reviews || reviews.length === 0) break;
450
+ allReviews.push(...reviews);
451
+ if (reviews.length < limit) break;
452
+ page++;
453
+ }
454
+ return allReviews;
442
455
  }
443
456
 
444
457
  async updatePullReview(