@pipeworx/mcp-china-exchange-data 0.1.1 → 0.1.3
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 +2 -2
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +128 -24
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Live market data straight from the two Chinese mainland exchanges — SZSE (Shenzhen) and SSE (Shanghai), one pack because they are a single jurisdiction. Fills what `@pipeworx/china-stocks` doesn't cover: industry/sector turnover ranking, index daily/weekly bars, a full-market board snapshot for screens, and company bulletins (公告) by code.
|
|
4
4
|
|
|
5
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1764+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
6
6
|
|
|
7
7
|
板块 主力资金 净流入 指数 日线 公告 — 深交所 上交所 A股行情数据接口
|
|
8
8
|
|
|
@@ -81,7 +81,7 @@ directly, instead of just this one's:
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Both URLs reach the same gateway and the same
|
|
84
|
+
Both URLs reach the same gateway and the same 1764+ data sources. The
|
|
85
85
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
86
86
|
reaches all of them from either one.
|
|
87
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-china-exchange-data",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "China Exchange Data MCP (SZSE + SSE). Keyless.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-8195c53163250126a605b218ef1356b0b73857bc1a28595cb321f7eff3f25320",
|
|
30
|
+
"sourceCommit": "0ce6905893fe8da0d285be2f05754a4200d67371"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pipeworx-io/china-exchange-data",
|
|
4
4
|
"title": "China Exchange Data",
|
|
5
5
|
"description": "China Exchange Data MCP (SZSE + SSE). Keyless.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.3",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/china-exchange-data",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-china-exchange-data",
|
package/src/index.ts
CHANGED
|
@@ -452,7 +452,42 @@ async function fetchWithTimeout(
|
|
|
452
452
|
),
|
|
453
453
|
);
|
|
454
454
|
}
|
|
455
|
-
|
|
455
|
+
// Fleet #2382. Everything that isn't a timeout/abort here is a genuine
|
|
456
|
+
// NETWORK-LEVEL failure — DNS resolution, connection refused, TLS handshake,
|
|
457
|
+
// Cloudflare's own "Network connection lost." — meaning `fetch()` itself
|
|
458
|
+
// threw and no HTTP response of any kind was ever received. Until this fix
|
|
459
|
+
// that raw exception was rethrown VERBATIM: a bare `TypeError: fetch failed`
|
|
460
|
+
// (or the Workers-runtime equivalent) names no upstream, carries no class
|
|
461
|
+
// token, and reads exactly like a defect in OUR code — because it says
|
|
462
|
+
// nothing about the call at all. It landed in `error`, the tier that means
|
|
463
|
+
// "Pipeworx has a defect", for every one of the (at the time of writing)
|
|
464
|
+
// ~470 packs that call this helper directly with no wrapper of their own.
|
|
465
|
+
//
|
|
466
|
+
// `dexscreener` hit this independently (fleet #1579) and fixed it with a
|
|
467
|
+
// bespoke per-pack try/catch around `fetchWithTimeout`. That fix is correct
|
|
468
|
+
// but only covers one pack; every other caller of this shared helper still
|
|
469
|
+
// leaked the raw exception. Moving the same fix HERE — the one place that
|
|
470
|
+
// already carries the timeout case — covers every pack that uses
|
|
471
|
+
// `fetchWithTimeout` without a wrapper, for free, and without widening
|
|
472
|
+
// `classifyToolError`'s regex list: the fix is giving the message a proper
|
|
473
|
+
// `upstream_down:` token at the point the two facts (no response was ever
|
|
474
|
+
// received, and which host we were trying to reach) are actually in hand,
|
|
475
|
+
// not teaching the classifier to guess from prose after the fact.
|
|
476
|
+
//
|
|
477
|
+
// Safe on the same grounds as the timeout branch above: no argument a
|
|
478
|
+
// caller passes can make `fetch()` itself throw a connection-level error,
|
|
479
|
+
// so this is always an availability failure, never a caller mistake. Same
|
|
480
|
+
// `markInternalOrigin` treatment — an origin we run that never answered is
|
|
481
|
+
// still ours, not a third party's outage.
|
|
482
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
483
|
+
throw new Error(
|
|
484
|
+
markInternalOrigin(
|
|
485
|
+
`upstream_down: could not reach ${name} at all (${raw.slice(0, 160)}). ` +
|
|
486
|
+
`No request reached ${name}, so this says NOTHING about whether the arguments you passed ` +
|
|
487
|
+
'are valid — do not re-check them on the strength of this error. Retry shortly.',
|
|
488
|
+
url,
|
|
489
|
+
),
|
|
490
|
+
);
|
|
456
491
|
}
|
|
457
492
|
}
|
|
458
493
|
|
|
@@ -652,14 +687,23 @@ function collapse(s: string): string {
|
|
|
652
687
|
* CATALOGID=<id> — used here for CATALOGID=1804_gptj_merge_after (股票行业
|
|
653
688
|
* 统计, industry turnover statistics; requires txtQueryDate).
|
|
654
689
|
* - SZSE quote history: www.szse.cn/api/market/ssjjhq/getHistoryData?
|
|
655
|
-
* cycleType=<32|33>&marketId=1&code=<code> — daily(32)/weekly(33)
|
|
656
|
-
* bars for any SZSE-listed code OR SZSE index (399xxx).
|
|
690
|
+
* cycleType=<32|33|37>&marketId=1&code=<code> — daily(32)/weekly(33)/
|
|
691
|
+
* annual(37) OHLC bars for any SZSE-listed code OR SZSE index (399xxx).
|
|
692
|
+
* No begin/end window param — always returns full available history
|
|
693
|
+
* (confirmed back to 1991 for 399xxx indices), sliced client-side to the
|
|
694
|
+
* requested count. (cycleType 34/35/36 = monthly/quarterly/semi-annual,
|
|
695
|
+
* probed but not exposed as tool params — not asked for.) Row shape
|
|
657
696
|
* (empirically confirmed, not documented by SZSE):
|
|
658
|
-
* [date, open, close, low, high, change, change_pct, volume, amount]
|
|
697
|
+
* [date, open, close, low, high, change, change_pct, volume, amount] —
|
|
698
|
+
* change/change_pct are period-over-period (e.g. for cycleType=37, this
|
|
699
|
+
* year's close vs last year's close), computed by SZSE itself.
|
|
659
700
|
* - SSE public quote system: yunhq.sse.com.cn:32041/v1/sh1/... (plain HTTP
|
|
660
|
-
* only — the TLS port is not reachable from here). /dayk/<code
|
|
661
|
-
*
|
|
662
|
-
*
|
|
701
|
+
* only — the TLS port is not reachable from here). /dayk/<code>?period=
|
|
702
|
+
* day|week|year gives OHLCV bars for any SSE code OR SSE index
|
|
703
|
+
* (000xxx/999xxx); period=year confirmed live back to the 1990s. Row =
|
|
704
|
+
* [date(YYYYMMDD), open, high, low, close, volume, amount] — no
|
|
705
|
+
* change/change_pct field, so ashares_index_bars computes it here from
|
|
706
|
+
* consecutive closes (fetches one extra bar as the prior-close anchor).
|
|
663
707
|
* /list/exchange/equity gives the full SSE equity board as one paged call
|
|
664
708
|
* (~2,358 rows): [code, name, last, change_pct, volume, amount] — a
|
|
665
709
|
* real-time/latest-session snapshot, not per-symbol OHLC, and not
|
|
@@ -718,6 +762,24 @@ function round2(v: number | null): number | null {
|
|
|
718
762
|
function todayISO(): string {
|
|
719
763
|
return new Date().toISOString().slice(0, 10);
|
|
720
764
|
}
|
|
765
|
+
/** True if `dateStr` (YYYY-MM-DD) is the exchange's in-progress CURRENT year,
|
|
766
|
+
* not yet a closed calendar year — both exchanges' annual-bar endpoints append
|
|
767
|
+
* a running bar for the current year dated "today" rather than year-end until
|
|
768
|
+
* the year actually closes (confirmed live 2026-10-10: the newest annual bar
|
|
769
|
+
* for 000001/399001 was dated 2026-10-09, not 2026-12-3x). Treating that as a
|
|
770
|
+
* completed "2026 annual close" would silently push a 6-year request to
|
|
771
|
+
* 2021-2026 instead of 2020-2025 — so `years` mode drops it. Once the
|
|
772
|
+
* calendar reaches the exchanges' actual year-end trading days (~Dec 29-31)
|
|
773
|
+
* the bar IS the real close and is kept. */
|
|
774
|
+
function isPartialCurrentYear(dateStr: string): boolean {
|
|
775
|
+
const today = todayISO();
|
|
776
|
+
if (dateStr.slice(0, 4) !== today.slice(0, 4)) return false;
|
|
777
|
+
return !(today.slice(5, 7) === '12' && Number(today.slice(8, 10)) >= 28);
|
|
778
|
+
}
|
|
779
|
+
function dropTrailingPartialYear<T extends { date: string }>(bars: T[]): T[] {
|
|
780
|
+
if (bars.length === 0) return bars;
|
|
781
|
+
return isPartialCurrentYear(bars[bars.length - 1].date) ? bars.slice(0, -1) : bars;
|
|
782
|
+
}
|
|
721
783
|
function stripJsonp(text: string): unknown {
|
|
722
784
|
const m = text.match(/^[^(]*\(([\s\S]*)\)\s*;?\s*$/);
|
|
723
785
|
const body = m ? m[1] : text;
|
|
@@ -786,13 +848,14 @@ const tools: McpToolExport['tools'] = [
|
|
|
786
848
|
{
|
|
787
849
|
name: 'ashares_index_bars',
|
|
788
850
|
description:
|
|
789
|
-
'
|
|
851
|
+
'上证/深证指数日线周线年线 — Daily, weekly, or annual OHLC + volume + turnover bars for a major China A-share index: 上证指数 Shanghai Composite (000001), 上证50 (000016), 沪深300 CSI 300 (000300), 科创50 STAR50 (000688) from SSE; 深证成指 Shenzhen Component (399001), 创业板指 ChiNext (399006) from SZSE. Weekly history goes back up to 6 years (312 bars); `years` returns one bar per calendar year (close + year-over-year pct change) for multi-year questions like "5 years of annual closes for CSI 300". Answers "上证指数000001最近10个交易日OHLC", "Shanghai Composite last 10 days", "深证成指周线", "ChiNext index history", "CSI 300 annual close 2020-2025". Source: SSE yunhq public quote system (000xxx/999xxx indices) + SZSE ssjjhq history API (399xxx indices), both keyless.',
|
|
790
852
|
inputSchema: {
|
|
791
853
|
type: 'object' as const,
|
|
792
854
|
properties: {
|
|
793
855
|
index: { type: 'string', description: 'Index code: 000001 (上证指数), 000016 (上证50), 000300 (沪深300), 000688 (科创50), 399001 (深证成指), 399006 (创业板指). Default 000001.' },
|
|
794
|
-
days: { type: 'number', description: 'Number of daily bars to return, 1-250 (default 10). Ignored if weeks is set.' },
|
|
795
|
-
weeks: { type: 'number', description: 'Number of weekly bars to return, 1-
|
|
856
|
+
days: { type: 'number', description: 'Number of daily bars to return, 1-250 (default 10). Ignored if weeks or years is set.' },
|
|
857
|
+
weeks: { type: 'number', description: 'Number of weekly bars to return, 1-312 (up to ~6 years). Overrides days when set; ignored if years is set.' },
|
|
858
|
+
years: { type: 'number', description: 'Number of annual bars to return, 1-40 — one bar per calendar year with close and year-over-year pct_change. Overrides weeks and days when set. E.g. years:6 for 2020-2025 inclusive.' },
|
|
796
859
|
},
|
|
797
860
|
},
|
|
798
861
|
},
|
|
@@ -869,13 +932,27 @@ async function sectorFlows(args: Record<string, unknown>) {
|
|
|
869
932
|
// ── ashares_index_bars ──────────────────────────────────────────────
|
|
870
933
|
async function indexBars(args: Record<string, unknown>) {
|
|
871
934
|
const code = typeof args.index === 'string' && args.index.trim() ? args.index.trim() : '000001';
|
|
872
|
-
|
|
873
|
-
|
|
935
|
+
// years > weeks > days. Weekly ceiling raised 104 -> 312 (~6y); years is new
|
|
936
|
+
// (both exchanges expose a native annual cycle — confirmed live 2026-10-10:
|
|
937
|
+
// SZSE cycleType=37, SSE yunhq period=year — so this is a real upstream bar
|
|
938
|
+
// size, not a client-side aggregation).
|
|
939
|
+
const years = args.years != null ? Math.min(Math.max(Number(args.years), 1), 40) : null;
|
|
940
|
+
const weeks = years == null && args.weeks != null ? Math.min(Math.max(Number(args.weeks), 1), 312) : null;
|
|
941
|
+
const days = years == null && weeks == null ? Math.min(Math.max(Number(args.days ?? 10), 1), 250) : null;
|
|
942
|
+
const want = (years ?? weeks ?? days) as number;
|
|
943
|
+
const period = years != null ? 'annual' : weeks != null ? 'weekly' : 'daily';
|
|
874
944
|
const exch = indexExchange(code);
|
|
875
945
|
const name = exch === 'sse' ? SSE_INDICES[code] : SZSE_INDICES[code];
|
|
876
946
|
|
|
877
947
|
if (exch === 'szse') {
|
|
878
|
-
|
|
948
|
+
// SZSE cycleType: 32 daily, 33 weekly, 37 annual (confirmed live; 34/35/36
|
|
949
|
+
// are monthly/quarterly/semi-annual, not used here). getHistoryData has no
|
|
950
|
+
// begin/end window param — it always returns full available history and
|
|
951
|
+
// we slice client-side, which is also how the weekly ceiling raise works
|
|
952
|
+
// with no upstream change. change/change_pct in the row are already
|
|
953
|
+
// period-over-period (confirmed: 2020 annual change = 2020 close - 2019
|
|
954
|
+
// close for 399001), so no recomputation needed here.
|
|
955
|
+
const cycleType = years != null ? 37 : weeks != null ? 33 : 32;
|
|
879
956
|
const params = new URLSearchParams({ random: String(Math.random()), cycleType: String(cycleType), marketId: '1', code });
|
|
880
957
|
const res = await pwFetch(`${SZSE_HISTORY}?${params}`, { headers: { Referer: SZSE_REFERER } }, 'SZSE');
|
|
881
958
|
if (!res.ok) throw await httpError(res, 'SZSE');
|
|
@@ -883,36 +960,63 @@ async function indexBars(args: Record<string, unknown>) {
|
|
|
883
960
|
if (body.code !== '0' || !body.data?.picupdata) {
|
|
884
961
|
return { index: code, name: name ?? body.data?.name ?? null, found: false, message: `No SZSE history for index ${code}.` };
|
|
885
962
|
}
|
|
886
|
-
|
|
887
|
-
|
|
963
|
+
// For annual, fetch a small margin so dropping the in-progress current
|
|
964
|
+
// year still leaves `want` closed years (full history is always returned
|
|
965
|
+
// here regardless, so this costs nothing extra upstream).
|
|
966
|
+
let bars = body.data.picupdata.slice(-(want + (years != null ? 2 : 0))).map((r) => szseRow(r));
|
|
967
|
+
if (years != null) bars = dropTrailingPartialYear(bars);
|
|
968
|
+
bars = bars.slice(-want);
|
|
888
969
|
return {
|
|
889
970
|
index: code,
|
|
890
971
|
name: name ?? body.data.name ?? null,
|
|
891
|
-
period
|
|
972
|
+
period,
|
|
892
973
|
bar_count: bars.length,
|
|
893
974
|
bars,
|
|
894
|
-
source:
|
|
975
|
+
source: years != null
|
|
976
|
+
? 'SZSE ssjjhq getHistoryData (cycleType=37 annual, marketId=1) — one bar per calendar year; change/change_pct are SZSE-computed year-over-year close deltas'
|
|
977
|
+
: 'SZSE ssjjhq getHistoryData (cycleType=32 daily / 33 weekly, marketId=1) — undocumented row order confirmed empirically as [date, open, close, low, high, change, change_pct, volume, amount]',
|
|
895
978
|
};
|
|
896
979
|
}
|
|
897
980
|
|
|
898
|
-
// SSE via yunhq: /v1/sh1/dayk/<code>?begin=-N&end=-1&period=day
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
981
|
+
// SSE via yunhq: /v1/sh1/dayk/<code>?begin=-N&end=-1&period=day|week|year
|
|
982
|
+
// (period=year confirmed live 2026-10-10 — e.g. 000001 has 37 annual bars
|
|
983
|
+
// back to the 1990s). Two things to compensate for, both confirmed live:
|
|
984
|
+
// (a) yunhq's "begin=-N" is off by one — it returns N-1 bars, not N (this
|
|
985
|
+
// IS the original bug symptom: weeks:104 returned bar_count 103).
|
|
986
|
+
// (b) yunhq's row has no change/change_pct field, so we compute it here
|
|
987
|
+
// from consecutive closes, which needs one extra prior-close anchor.
|
|
988
|
+
// For annual we also need one more so dropping the in-progress current
|
|
989
|
+
// year still leaves `want` closed years.
|
|
990
|
+
const upstreamPeriod = years != null ? 'year' : weeks != null ? 'week' : 'day';
|
|
991
|
+
const offByOne = 1; // (a)
|
|
992
|
+
const anchorExtra = 1; // (b)
|
|
993
|
+
const partialYearExtra = years != null ? 1 : 0;
|
|
994
|
+
const requestN = want + offByOne + anchorExtra + partialYearExtra;
|
|
995
|
+
const url = `${SSE_YUNHQ}/v1/sh1/dayk/${encodeURIComponent(code)}?begin=-${requestN}&end=-1&period=${upstreamPeriod}`;
|
|
902
996
|
const res = await pwFetch(url, {}, 'SSE');
|
|
903
997
|
if (!res.ok) throw await httpError(res, 'SSE');
|
|
904
998
|
const body = (await res.json()) as { code?: string; kline?: unknown[][] };
|
|
905
999
|
if (!body.kline || body.kline.length === 0) {
|
|
906
1000
|
return { index: code, name: name ?? null, found: false, message: `No SSE history for index ${code}.` };
|
|
907
1001
|
}
|
|
908
|
-
const
|
|
1002
|
+
const parsed = body.kline.map((r) => sseRow(r));
|
|
1003
|
+
let withChange = parsed.map((r, i) => {
|
|
1004
|
+
const prevClose = i > 0 ? parsed[i - 1].close : null;
|
|
1005
|
+
const change = prevClose != null && r.close != null ? round2(r.close - prevClose) : null;
|
|
1006
|
+
const change_pct = prevClose != null && r.close != null && prevClose !== 0 ? round2(((r.close - prevClose) / prevClose) * 100) : null;
|
|
1007
|
+
return { ...r, change, change_pct };
|
|
1008
|
+
});
|
|
1009
|
+
if (years != null) withChange = dropTrailingPartialYear(withChange);
|
|
1010
|
+
const bars = withChange.length > want ? withChange.slice(-want) : withChange;
|
|
909
1011
|
return {
|
|
910
1012
|
index: code,
|
|
911
1013
|
name: name ?? null,
|
|
912
|
-
period
|
|
1014
|
+
period,
|
|
913
1015
|
bar_count: bars.length,
|
|
914
1016
|
bars,
|
|
915
|
-
source:
|
|
1017
|
+
source: years != null
|
|
1018
|
+
? 'SSE yunhq public quote system /v1/sh1/dayk period=year — one bar per calendar year; change/change_pct computed here (close vs prior year close) since SSE does not return them'
|
|
1019
|
+
: 'SSE yunhq public quote system /v1/sh1/dayk (row = [date, open, high, low, close, volume, amount]; change/change_pct computed here from consecutive closes)',
|
|
916
1020
|
};
|
|
917
1021
|
}
|
|
918
1022
|
|
package/src/server.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
11
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-china-exchange-data', version: '0.1.
|
|
12
|
+
{ name: '@pipeworx/mcp-china-exchange-data', version: '0.1.3' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|