national-stats-mcp 1.1.0 → 2.0.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/README.md CHANGED
@@ -1,9 +1,18 @@
1
1
  # 国家统计局MCP/Agent数据获取工具
2
2
 
3
3
  ## 项目概述
4
- 本项目是一个基于MCP(Model Context Protocol)协议的国家统计局数据获取服务,为AI Agent提供直接访问国家统计局API的功能。项目支持获取各种统计数据,包含关键词搜索、分类浏览、时间维度查询等功能,遵循国家统计局官方API接口最佳实践。
4
+ 本项目是一个基于MCP(Model Context Protocol)协议的国家统计局数据获取服务,为AI Agent提供直接访问国家统计局API的功能。项目**同时支持新版API (V2.0)和旧版API**,支持获取各种统计数据,包含关键词搜索、分类浏览、时间维度查询等功能。
5
5
 
6
- 主要特点:
6
+ ### 🆕 新版 API V2.0 (2026.03.27更新)
7
+ 新版API采用UUID标识符架构,具有以下特点:
8
+ - ✅ **UUID标识**: 使用UUID作为唯一标识符,摒弃旧的层级代码系统
9
+ - ✅ **三步查询**: 树形导航 → 指标元数据 → 批量取值
10
+ - ✅ **时间分片**: 同一指标按5年或统计制度变革周期分割,支持更精细的数据管理
11
+ - ✅ **批量查询**: 支持一次性查询多个指标、多个时间点数据
12
+ - ✅ **缓存优化**: 内置智能缓存机制,提升查询效率
13
+ - ✅ **完整元数据**: 包含统计口径说明、单位信息等关键元数据
14
+
15
+ ### 旧版 API (向后兼容)
7
16
  - ✅ 关键词搜索:直接搜索获取指标代码及最新值(推荐用于快速查询)
8
17
  - ✅ 树形分类:可浏览和递归获取所有分类/指标体系
9
18
  - ✅ 预遍历数据:提供完整的预遍历分类编码体系,可直接读取使用(减少递归调用)
@@ -62,12 +71,129 @@ node dist/index.js --port 8080
62
71
  批量获取多个统计指标的数据。
63
72
  - 参数:`queries`(必需)- 查询参数数组
64
73
 
74
+ ## 🆕 新版 API V2.0 使用指南
75
+
76
+ ### 核心概念
77
+
78
+ 新版API引入以下关键概念:
79
+
80
+ | 标识符 | 说明 |
81
+ |--------|------|
82
+ | **pid** | 父节点ID,用于在目录树中向下展开 |
83
+ | **cid** | 数据集ID,代表一个叶子节点(特定指标+地区+时间段) |
84
+ | **indicatorId** | 具体指标ID,代表数据集中的某一列 |
85
+ | **时间分片** | 同一指标按时间分成多个cid(如2021-2025, 2026-至今) |
86
+
87
+ ### 新版API客户端使用示例
88
+
89
+ ```typescript
90
+ import { NewStatsApiClient, CategoryCode } from './src/api-client';
91
+
92
+ // 创建客户端实例
93
+ const client = new NewStatsApiClient();
94
+
95
+ // 方式一:一步到位查询(推荐)
96
+ async function quickQuery() {
97
+ const result = await client.searchAndGet(
98
+ 'CPI', // 关键词
99
+ '居民消费价格指数', // 指标名称(可选)
100
+ '202601MM', // 起始时间(可选)
101
+ '202603MM' // 结束时间(可选)
102
+ );
103
+
104
+ console.log('数据集ID:', result.cid);
105
+ console.log('指标信息:', result.indicator);
106
+ console.log('数据:', result.data);
107
+ }
108
+
109
+ // 方式二:分步查询(适合复杂场景)
110
+ async function stepByStep() {
111
+ // 1. 搜索定位数据集
112
+ const searchResults = await client.search({
113
+ search: 'GDP',
114
+ pageSize: 10
115
+ });
116
+
117
+ // 2. 获取树结构(遍历分类)
118
+ const treeNodes = await client.getTree({
119
+ code: CategoryCode.MONTHLY, // 月度数据
120
+ pid: '' // 根节点
121
+ });
122
+
123
+ // 3. 获取指标列表
124
+ const indicators = await client.getIndicators({
125
+ cid: 'your-cid-here'
126
+ });
127
+
128
+ // 4. 查询数据
129
+ const data = await client.getData({
130
+ cid: 'your-cid-here',
131
+ indicatorIds: ['indicator-id-1', 'indicator-id-2'],
132
+ das: [{ text: '全国', value: '000000000000' }],
133
+ dts: ['202601MM-202612MM']
134
+ });
135
+ }
136
+
137
+ // 方式三:获取所有叶子节点(慎用,耗时)
138
+ async function getAllLeafs() {
139
+ const leafs = await client.getAllLeafNodes(CategoryCode.MONTHLY);
140
+ console.log(`共找到 ${leafs.length} 个数据集`);
141
+ }
142
+ ```
143
+
144
+ ### 时间编码格式
145
+
146
+ 新版API使用特定的时间编码格式:
147
+
148
+ | 类型 | 格式 | 示例 |
149
+ |------|------|------|
150
+ | 月度 | YYYYMM + MM | 202602MM (2026年2月) |
151
+ | 季度 | YYYYQ + SS | 20254SS (2025年第4季度) |
152
+ | 年度 | YYYY + YY | 2025YY (2025年) |
153
+ | 范围 | Start-End | 202601MM-202612MM |
154
+
155
+ ### 分类代码
156
+
157
+ ```typescript
158
+ enum CategoryCode {
159
+ MONTHLY = '1', // 月度数据
160
+ QUARTERLY = '2', // 季度数据
161
+ YEARLY = '3', // 年度数据
162
+ PROVINCE_QUARTERLY = '5', // 分省季度
163
+ PROVINCE_YEARLY = '6', // 分省年度
164
+ OTHER = '7', // 其他/普查
165
+ }
166
+ ```
167
+
168
+ ### 最佳实践
169
+
170
+ 1. **优先使用搜索接口**: 用 `search()` 快速定位cid,避免全量遍历树
171
+ 2. **处理时间分片**: 同一指标可能有多个cid,需按时间拼接数据
172
+ 3. **批量请求**: `getData()` 支持多个indicatorIds,一次请求获取多个指标
173
+ 4. **关注元数据**: 查看indicator的`i_mark`字段了解统计口径
174
+ 5. **利用缓存**: 客户端内置缓存,树结构和指标列表会自动缓存
175
+
65
176
  ## 模块介绍
66
177
  - `src/index.ts`: MCP服务器主入口,实现MCP协议规范
67
- - `src/api-client.ts`: 国家统计局API交互模块,提供完整的数据获取功能,包括搜索、分类、数据查询等
178
+ - `src/api-client.ts`: 国家统计局API交互模块,提供完整的数据获取功能
179
+ - **NewStatsApiClient**: 新版API V2.0客户端(推荐)
180
+ - **StatsApiClient**: 旧版API客户端(向后兼容)
181
+ - `src/types.ts`: 完整的类型定义,包含新旧两版API的数据结构
68
182
 
69
183
  ## 遵循国家统计局API标准
70
- 项目严格实现api_introduce.md中的接口规范,包含:
184
+
185
+ ### 新版 API V2.0
186
+ 项目严格实现新版API接口规范:
187
+ - 搜索接口 `/external/query` 用于关键词搜索和快速定位cid
188
+ - 树遍历接口 `/new/queryIndexTreeAsync` 用于浏览分类树和获取cid
189
+ - 指标查询接口 `/new/queryIndicatorsByCid` 用于获取指标列表
190
+ - 数据查询接口 `/stream/esData` 用于批量获取时间序列数据
191
+ - 内置缓存机制:树结构24小时,指标列表12小时
192
+ - UUID标识符系统和时间分片机制
193
+ - 完整的错误处理和重试逻辑
194
+
195
+ ### 旧版 API (向后兼容)
196
+ 项目严格实现旧版API接口规范:
71
197
  - 搜索接口 `/search.htm` 用于关键词查找和快速获取最新值
72
198
  - 分类接口 `/easyquery.htm` 用于遍历指标分类树
73
199
  - 数据接口 `/easyquery.htm` 用于查询历史时间序列
@@ -39,7 +39,7 @@ export interface StatsSearchResult {
39
39
  report: string;
40
40
  }>;
41
41
  }
42
- export interface SearchResponse {
42
+ export interface ParsedSearchResult {
43
43
  name: string;
44
44
  value: string;
45
45
  time: string;
@@ -80,7 +80,7 @@ export declare class StatsApiClient {
80
80
  /**
81
81
  * 解析搜索结果
82
82
  */
83
- parseSearchResult(result: StatsSearchResult): SearchResponse[];
83
+ parseSearchResult(result: StatsSearchResult): ParsedSearchResult[];
84
84
  /**
85
85
  * 获取指标分类树
86
86
  */
@@ -118,4 +118,69 @@ export declare class StatsApiClient {
118
118
  error?: string;
119
119
  }>>;
120
120
  }
121
+ import { TreeNode, Indicator, DataQueryParams, SearchItem, TreeQueryParams, IndicatorQueryParams, SearchQueryParams, CategoryCode, NewApiClientConfig, DataPoint } from './types';
122
+ /**
123
+ * 新版国家统计局API客户端 (V2.0)
124
+ * 基于UUID标识符的新架构
125
+ */
126
+ export declare class NewStatsApiClient {
127
+ private baseUrl;
128
+ private timeout;
129
+ private rootId;
130
+ private treeCache;
131
+ private indicatorsCache;
132
+ private readonly DEFAULT_TREE_TTL;
133
+ private readonly DEFAULT_INDICATORS_TTL;
134
+ constructor(config?: NewApiClientConfig);
135
+ /**
136
+ * 搜索数据集
137
+ * 用于快速定位 cid
138
+ */
139
+ search(params: SearchQueryParams): Promise<SearchItem[]>;
140
+ /**
141
+ * 获取树结构
142
+ * 用于遍历目录树,获取叶子节点(cid)
143
+ */
144
+ getTree(params: TreeQueryParams): Promise<TreeNode[]>;
145
+ /**
146
+ * 递归获取所有叶子节点(cid)
147
+ * 注意:此方法耗时较长,慎用
148
+ */
149
+ getAllLeafNodes(code: CategoryCode): Promise<TreeNode[]>;
150
+ /**
151
+ * 获取指标列表
152
+ * 根据 cid 获取所有可用指标
153
+ */
154
+ getIndicators(params: IndicatorQueryParams): Promise<Indicator[]>;
155
+ /**
156
+ * 查询数据
157
+ * 批量获取多个指标、多个时间点的数据
158
+ */
159
+ getData(params: DataQueryParams): Promise<DataPoint[]>;
160
+ /**
161
+ * 辅助方法: 搜索并获取数据
162
+ * 一步到位:搜索关键词 -> 获取cid -> 获取指标 -> 查询数据
163
+ */
164
+ searchAndGet(keyword: string, indicatorName?: string, startTime?: string, endTime?: string): Promise<{
165
+ cid: string;
166
+ indicator: Indicator;
167
+ data: DataPoint[];
168
+ }>;
169
+ /**
170
+ * 辅助方法: 从 globalid 提取 cid (最后一段)
171
+ */
172
+ private extractCidFromGlobalId;
173
+ /**
174
+ * 清除所有缓存
175
+ */
176
+ clearCache(): void;
177
+ /**
178
+ * 设置根节点ID
179
+ */
180
+ setRootId(rootId: string): void;
181
+ /**
182
+ * 获取当前根节点ID
183
+ */
184
+ getRootId(): string;
185
+ }
121
186
  //# sourceMappingURL=api-client.d.ts.map
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.StatsApiClient = void 0;
6
+ exports.NewStatsApiClient = exports.StatsApiClient = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  // 国家统计局API客户端
9
9
  class StatsApiClient {
@@ -224,4 +224,294 @@ class StatsApiClient {
224
224
  }
225
225
  }
226
226
  exports.StatsApiClient = StatsApiClient;
227
+ // ============= 新版 API 客户端 (V2.0) =============
228
+ const types_1 = require("./types");
229
+ /**
230
+ * 简单的内存缓存实现
231
+ */
232
+ class SimpleCache {
233
+ cache = new Map();
234
+ get(key) {
235
+ const item = this.cache.get(key);
236
+ if (!item)
237
+ return null;
238
+ if (Date.now() > item.expireAt) {
239
+ this.cache.delete(key);
240
+ return null;
241
+ }
242
+ return item.data;
243
+ }
244
+ set(key, data, ttl) {
245
+ this.cache.set(key, {
246
+ data,
247
+ expireAt: Date.now() + ttl,
248
+ });
249
+ }
250
+ clear() {
251
+ this.cache.clear();
252
+ }
253
+ }
254
+ /**
255
+ * 新版国家统计局API客户端 (V2.0)
256
+ * 基于UUID标识符的新架构
257
+ */
258
+ class NewStatsApiClient {
259
+ baseUrl;
260
+ timeout;
261
+ rootId;
262
+ // 缓存实例
263
+ treeCache;
264
+ indicatorsCache;
265
+ // 默认配置
266
+ DEFAULT_TREE_TTL = 24 * 60 * 60 * 1000; // 24小时
267
+ DEFAULT_INDICATORS_TTL = 12 * 60 * 60 * 1000; // 12小时
268
+ constructor(config) {
269
+ this.baseUrl = config?.baseUrl || types_1.NEW_API_BASE_URL;
270
+ this.timeout = config?.timeout || 30000;
271
+ this.rootId = config?.rootId || 'fc982599aa684be7969d7b90b1bd0e84'; // 月度数据根节点
272
+ // 初始化缓存
273
+ this.treeCache = new SimpleCache();
274
+ this.indicatorsCache = new SimpleCache();
275
+ }
276
+ /**
277
+ * 搜索数据集
278
+ * 用于快速定位 cid
279
+ */
280
+ async search(params) {
281
+ const url = new URL(`${this.baseUrl}/query`);
282
+ url.searchParams.set('search', params.search);
283
+ url.searchParams.set('pagenum', (params.pagenum || 1).toString());
284
+ url.searchParams.set('pageSize', (params.pageSize || 10).toString());
285
+ console.error(`Search Request: ${url.toString()}`);
286
+ try {
287
+ const response = await axios_1.default.get(url.toString(), {
288
+ timeout: this.timeout,
289
+ });
290
+ return response.data.data?.data || [];
291
+ }
292
+ catch (error) {
293
+ if (axios_1.default.isAxiosError(error)) {
294
+ throw new Error(`Search request failed: ${error.response?.data || error.message}`);
295
+ }
296
+ throw error;
297
+ }
298
+ }
299
+ /**
300
+ * 获取树结构
301
+ * 用于遍历目录树,获取叶子节点(cid)
302
+ */
303
+ async getTree(params) {
304
+ // 检查缓存
305
+ const cacheKey = `tree_${params.code}_${params.pid || 'root'}`;
306
+ const cached = this.treeCache.get(cacheKey);
307
+ if (cached) {
308
+ console.error(`Tree cache hit for ${cacheKey}`);
309
+ return cached;
310
+ }
311
+ const url = new URL(`${this.baseUrl}/new/queryIndexTreeAsync`);
312
+ if (params.pid) {
313
+ url.searchParams.set('pid', params.pid);
314
+ }
315
+ url.searchParams.set('code', params.code);
316
+ console.error(`Tree Request: ${url.toString()}`);
317
+ try {
318
+ const response = await axios_1.default.get(url.toString(), {
319
+ timeout: this.timeout,
320
+ });
321
+ const nodes = response.data.data || [];
322
+ // 缓存结果
323
+ this.treeCache.set(cacheKey, nodes, this.DEFAULT_TREE_TTL);
324
+ return nodes;
325
+ }
326
+ catch (error) {
327
+ if (axios_1.default.isAxiosError(error)) {
328
+ throw new Error(`Tree request failed: ${error.response?.data || error.message}`);
329
+ }
330
+ throw error;
331
+ }
332
+ }
333
+ /**
334
+ * 递归获取所有叶子节点(cid)
335
+ * 注意:此方法耗时较长,慎用
336
+ */
337
+ async getAllLeafNodes(code) {
338
+ const allLeaves = [];
339
+ const getRecursively = async (pid) => {
340
+ try {
341
+ // 添加延迟避免请求过于频繁
342
+ await new Promise(resolve => setTimeout(resolve, 300));
343
+ const nodes = await this.getTree({ pid, code });
344
+ for (const node of nodes) {
345
+ if (node.isLeaf) {
346
+ allLeaves.push(node);
347
+ }
348
+ else {
349
+ await getRecursively(node._id);
350
+ }
351
+ }
352
+ }
353
+ catch (error) {
354
+ console.error(`Error in recursive tree traversal for pid=${pid}:`, error);
355
+ throw error;
356
+ }
357
+ };
358
+ await getRecursively();
359
+ return allLeaves;
360
+ }
361
+ /**
362
+ * 获取指标列表
363
+ * 根据 cid 获取所有可用指标
364
+ */
365
+ async getIndicators(params) {
366
+ // 检查缓存
367
+ const cacheKey = `indicators_${params.cid}`;
368
+ const cached = this.indicatorsCache.get(cacheKey);
369
+ if (cached) {
370
+ console.error(`Indicators cache hit for ${cacheKey}`);
371
+ return cached;
372
+ }
373
+ const url = new URL(`${this.baseUrl}/new/queryIndicatorsByCid`);
374
+ url.searchParams.set('cid', params.cid);
375
+ if (params.dt) {
376
+ url.searchParams.set('dt', params.dt);
377
+ }
378
+ if (params.name) {
379
+ url.searchParams.set('name', params.name);
380
+ }
381
+ console.error(`Indicators Request: ${url.toString()}`);
382
+ try {
383
+ const response = await axios_1.default.get(url.toString(), {
384
+ timeout: this.timeout,
385
+ });
386
+ const indicators = response.data.data?.list || [];
387
+ // 缓存结果
388
+ this.indicatorsCache.set(cacheKey, indicators, this.DEFAULT_INDICATORS_TTL);
389
+ return indicators;
390
+ }
391
+ catch (error) {
392
+ if (axios_1.default.isAxiosError(error)) {
393
+ throw new Error(`Indicators request failed: ${error.response?.data || error.message}`);
394
+ }
395
+ throw error;
396
+ }
397
+ }
398
+ /**
399
+ * 查询数据
400
+ * 批量获取多个指标、多个时间点的数据
401
+ */
402
+ async getData(params) {
403
+ const payload = {
404
+ cid: params.cid,
405
+ indicatorIds: params.indicatorIds,
406
+ das: params.das,
407
+ dts: params.dts,
408
+ showType: params.showType || '1',
409
+ rootId: params.rootId || this.rootId,
410
+ };
411
+ console.error(`Data Request: ${this.baseUrl}/stream/esData`);
412
+ console.error(`Payload:`, JSON.stringify(payload, null, 2));
413
+ try {
414
+ const response = await axios_1.default.post(`${this.baseUrl}/stream/esData`, payload, {
415
+ timeout: this.timeout,
416
+ headers: {
417
+ 'Content-Type': 'application/json',
418
+ },
419
+ });
420
+ if (!response.data.success) {
421
+ throw new Error('Data query failed: API returned success=false');
422
+ }
423
+ return response.data.data || [];
424
+ }
425
+ catch (error) {
426
+ if (axios_1.default.isAxiosError(error)) {
427
+ throw new Error(`Data request failed: ${error.response?.data || error.message}`);
428
+ }
429
+ throw error;
430
+ }
431
+ }
432
+ /**
433
+ * 辅助方法: 搜索并获取数据
434
+ * 一步到位:搜索关键词 -> 获取cid -> 获取指标 -> 查询数据
435
+ */
436
+ async searchAndGet(keyword, indicatorName, startTime, endTime) {
437
+ // 1. 搜索定位cid
438
+ const searchResults = await this.search({ search: keyword, pageSize: 10 });
439
+ if (!searchResults || searchResults.length === 0) {
440
+ throw new Error(`No results found for keyword: ${keyword}`);
441
+ }
442
+ // 优先选择最新的数据集(通过edate判断)
443
+ const target = searchResults.reduce((latest, current) => {
444
+ if (!latest.edate)
445
+ return current;
446
+ if (!current.edate)
447
+ return latest;
448
+ return current.edate > latest.edate ? current : latest;
449
+ });
450
+ // 从 globalid 提取 cid (如果搜索结果没有直接提供cid)
451
+ const cid = target.cid || this.extractCidFromGlobalId(target.treeinfo_globalid);
452
+ if (!cid) {
453
+ throw new Error('Failed to extract cid from search result');
454
+ }
455
+ // 2. 获取指标列表
456
+ const indicators = await this.getIndicators({ cid });
457
+ if (!indicators || indicators.length === 0) {
458
+ throw new Error(`No indicators found for cid: ${cid}`);
459
+ }
460
+ // 筛选目标指标
461
+ let targetIndicator;
462
+ if (indicatorName) {
463
+ targetIndicator = indicators.find(ind => ind.i_showname.includes(indicatorName));
464
+ }
465
+ else {
466
+ targetIndicator = indicators[0]; // 默认取第一个
467
+ }
468
+ if (!targetIndicator) {
469
+ throw new Error(`Indicator not found: ${indicatorName}`);
470
+ }
471
+ // 3. 查询数据
472
+ const timeRange = startTime && endTime
473
+ ? `${startTime}-${endTime}`
474
+ : `${target.sdate || '202001'}MM-${target.edate || '202612'}MM`;
475
+ const data = await this.getData({
476
+ cid,
477
+ indicatorIds: [targetIndicator._id],
478
+ das: [{ text: '全国', value: '000000000000' }],
479
+ dts: [timeRange],
480
+ });
481
+ return {
482
+ cid,
483
+ indicator: targetIndicator,
484
+ data,
485
+ };
486
+ }
487
+ /**
488
+ * 辅助方法: 从 globalid 提取 cid (最后一段)
489
+ */
490
+ extractCidFromGlobalId(globalId) {
491
+ if (!globalId)
492
+ return null;
493
+ const parts = globalId.split('.');
494
+ return parts[parts.length - 1] || null;
495
+ }
496
+ /**
497
+ * 清除所有缓存
498
+ */
499
+ clearCache() {
500
+ this.treeCache.clear();
501
+ this.indicatorsCache.clear();
502
+ }
503
+ /**
504
+ * 设置根节点ID
505
+ */
506
+ setRootId(rootId) {
507
+ this.rootId = rootId;
508
+ }
509
+ /**
510
+ * 获取当前根节点ID
511
+ */
512
+ getRootId() {
513
+ return this.rootId;
514
+ }
515
+ }
516
+ exports.NewStatsApiClient = NewStatsApiClient;
227
517
  //# sourceMappingURL=api-client.js.map