linker-position-mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +264 -0
  2. package/dist/index.js +259 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # linker-position-mcp
2
+
3
+ Linker 持仓 + 盈亏 + VAR 风险查询 MCP Server。
4
+
5
+ 面向 AI Agent / 大模型,提供公司级和个人级的持仓、盈亏、VAR 风险查询能力。所有工具只认人名不认 ID,对大模型友好。
6
+
7
+ ## 快速开始
8
+
9
+ ### 环境变量
10
+
11
+ ```
12
+ TRADING_API_BASE=http://hub.linker.net/trading-api
13
+ TRADING_API_KEY=trading-prod-20260323
14
+ ```
15
+
16
+ ### 配置方式
17
+
18
+ **npx(发布后):**
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "linker-position": {
24
+ "command": "npx",
25
+ "args": ["-y", "linker-position-mcp@latest"],
26
+ "env": {
27
+ "TRADING_API_BASE": "http://hub.linker.net/trading-api",
28
+ "TRADING_API_KEY": "trading-prod-20260323"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ **本地开发(直接用编译产物):**
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "linker-position": {
41
+ "command": "node",
42
+ "args": ["C:/Users/94503/Documents/GitHub/linker_mcp/packages/linker-position-mcp/dist/index.js"],
43
+ "env": {
44
+ "TRADING_API_BASE": "http://hub.linker.net/trading-api",
45
+ "TRADING_API_KEY": "trading-prod-20260323"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## 工具列表
53
+
54
+ | Tool | 说明 | 参数 |
55
+ |------|------|------|
56
+ | `get_pnl_overview` | 公司整体盈亏+持仓+VAR 快照 | 无 |
57
+ | `get_owner_detail` | 某人持仓+盈亏+VAR 明细 | `owner: string` |
58
+ | `get_risk_status` | VAR 风险状态 | `owner?: string` |
59
+ | `list_owners` | 列出所有持仓人姓名 | 无 |
60
+
61
+ ## 覆盖维度
62
+
63
+ | 维度 | 全局 | 某人 |
64
+ |------|------|------|
65
+ | 持仓+盈亏 | `get_pnl_overview()` — 各人一行汇总排名 | `get_owner_detail(owner)` — 策略→legs 明细 |
66
+ | VAR 风险 | `get_risk_status()` — 全员概览+超限告警 | `get_risk_status(owner)` — 某人 30 日趋势 |
67
+ | 辅助 | `list_owners()` — 确认人名 | — |
68
+
69
+ ## 场景对照表
70
+
71
+ | 用户问 | 调哪个工具 |
72
+ |--------|-----------|
73
+ | "公司现在整体什么情况" | `get_pnl_overview` |
74
+ | "各人盈亏排名" | `get_pnl_overview` |
75
+ | "谁在赚谁在亏" | `get_pnl_overview` |
76
+ | "任志华持仓什么" | `get_owner_detail(owner:"任志华")` |
77
+ | "任志华赚了多少" | `get_owner_detail(owner:"任志华")` |
78
+ | "某人各策略盈亏明细" | `get_owner_detail(owner:"某人")` |
79
+ | "某人浮盈多少" | `get_owner_detail(owner:"某人")` |
80
+ | "谁的 VAR 超限了" | `get_risk_status()` |
81
+ | "全员风险概览" | `get_risk_status()` |
82
+ | "某人 VAR 趋势" | `get_risk_status(owner:"某人")` |
83
+ | "有哪些交易员" | `list_owners` |
84
+
85
+ ## 各工具返回结构
86
+
87
+ ### get_pnl_overview
88
+
89
+ ```json
90
+ {
91
+ "company": {
92
+ "realized_pnl": 89555573.88,
93
+ "floating_pnl": -37185340.99,
94
+ "total_pnl": 52370232.89,
95
+ "var_used": 52398632.93,
96
+ "var_remain": 23451367.07,
97
+ "var_limit": "7585万",
98
+ "var_usage": "69%"
99
+ },
100
+ "owners": [
101
+ {
102
+ "owner": "任志华",
103
+ "active_strategies": 15,
104
+ "total_strategies": 83,
105
+ "total_pnl": 101024976.04,
106
+ "floating_pnl": 13381939.35,
107
+ "var": 12942379.87,
108
+ "var_usage": 0.43,
109
+ "alert": "normal"
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ### get_owner_detail
116
+
117
+ ```json
118
+ {
119
+ "owner": "任志华",
120
+ "realized_pnl": 190948992.89,
121
+ "floating_pnl": 13381939.35,
122
+ "total_pnl": 204330932.24,
123
+ "var": {
124
+ "total": 12942379.87,
125
+ "hedge": null,
126
+ "arbitrage": null,
127
+ "outright": 12942379.87
128
+ },
129
+ "var_limit": 30000000,
130
+ "var_usage_rate": 0.43,
131
+ "var_alert": "normal",
132
+ "active_strategies": 15,
133
+ "total_strategies": 83,
134
+ "strategies": [
135
+ {
136
+ "strategy": "AU_单边策略_趋势跟踪_AU2606",
137
+ "category": "单边策略",
138
+ "product": "AU",
139
+ "total_pnl": -6368497,
140
+ "floating_pnl": -4397171,
141
+ "var": 3200000,
142
+ "legs": [
143
+ {
144
+ "contract": "AU2608",
145
+ "direction": "BUY",
146
+ "remaining_qty": 36,
147
+ "avg_price": 996.38,
148
+ "market_price": 874.24,
149
+ "unrealized_pnl": -4397171,
150
+ "realized_pnl": 0
151
+ }
152
+ ]
153
+ }
154
+ ]
155
+ }
156
+ ```
157
+
158
+ ### get_risk_status(全员)
159
+
160
+ ```json
161
+ {
162
+ "owners": [
163
+ {
164
+ "owner": "汪浩",
165
+ "department": "交易三部",
166
+ "var_value": 1318.1,
167
+ "var_limit": 150,
168
+ "usage_rate": 8.79,
169
+ "alert_level": "overlimit"
170
+ }
171
+ ],
172
+ "alerts": [
173
+ {
174
+ "owner": "汪浩",
175
+ "var_value": 1318.1,
176
+ "var_limit": 150,
177
+ "usage_rate": 8.79,
178
+ "alert_level": "overlimit"
179
+ }
180
+ ]
181
+ }
182
+ ```
183
+
184
+ ### get_risk_status(某人趋势)
185
+
186
+ ```json
187
+ {
188
+ "owner": "黄皓生",
189
+ "days": 30,
190
+ "trend": [
191
+ { "date": "2026-06-20", "var_value": 52.3, "var_limit": 300, "usage_rate": 0.17, "alert_level": "normal" },
192
+ { "date": "2026-07-19", "var_value": 64.29, "var_limit": 300, "usage_rate": 0.21, "alert_level": "normal" }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ ### list_owners
198
+
199
+ ```json
200
+ ["任志华", "于熙", "何佳驹", "吴培", "周菲", "..."]
201
+ ```
202
+
203
+ ## 技术细节
204
+
205
+ - **数据来源**:所有数据走服务端聚合接口,不拉明细(无 60 万行查询)
206
+ - `/api/v1/pnl/report` — 持仓 + 盈亏 + VAR(服务端聚合 owner→strategies→legs)
207
+ - `/api/v1/risk/dashboard/overview` — VAR 概览
208
+ - `/api/v1/risk/dashboard/alerts` — 超限告警
209
+ - `/api/v1/risk/dashboard/history` — VAR 趋势
210
+ - `/api/v1/owners` — 持仓人列表
211
+ - **认证**:`X-API-Key` header
212
+ - **只读**:所有工具 readOnly,不做任何写操作
213
+ - **owner 解析**:内部缓存 ownerMap(name↔id),用户只需传姓名
214
+
215
+ ## 与 trading-mcp-v2 的关系
216
+
217
+ 本 MCP 专注**持仓/盈亏/风险**查询,可独立运行,也可与 trading-mcp-v2 并行使用。
218
+
219
+ | 能力 | trading-mcp-v2 | position-mcp |
220
+ |------|---------------|--------------|
221
+ | 持仓+盈亏快照 | ✅ get_owner_pnl/get_pnl_summary | ✅ get_pnl_overview/get_owner_detail |
222
+ | VAR 风险仪表盘 | 部分(嵌在盈亏报告里) | ✅ 独立维度 get_risk_status |
223
+ | 策略详情/公式/评分 | ✅ | ❌ |
224
+ | 行情/历史价格 | ✅ | ❌ |
225
+ | 经济数据库 | ✅ | ❌ |
226
+ | 外盘实时行情 | ✅ | ❌ |
227
+
228
+ ## 开发
229
+
230
+ ```bash
231
+ cd packages/linker-position-mcp
232
+ npm install
233
+ npm run build # tsc 编译
234
+ npm run watch # 开发热编译
235
+ node test_tool.mjs # 运行工具测试
236
+ ```
237
+
238
+ ## 测试
239
+
240
+ ```bash
241
+ node test_tool.mjs
242
+ ```
243
+
244
+ 输出示例:
245
+ ```
246
+ === linker-position-mcp Tool Tests ===
247
+ ═══ Test 1: list_owners ═══
248
+ ✓ 39 owners found
249
+
250
+ ═══ Test 2: get_pnl_overview ═══
251
+ ✓ grand_total: realized=89555573, floating=-37185340, total=52370232
252
+ ✓ 37 owners summarized
253
+
254
+ ═══ Test 3: get_owner_detail(任志华) ═══
255
+ ✓ 任志华: 15 active strategies, total_pnl=204330932
256
+
257
+ ═══ Test 4: get_risk_status (全员) ═══
258
+ ✓ 25 owners, 5 alerts (overlimit)
259
+
260
+ ═══ Test 5: get_risk_status(任志华) — VAR趋势 ═══
261
+ ✓ 任志华 近30日无VAR记录
262
+
263
+ ✅ All tests passed
264
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Linker Position & PnL MCP Server
4
+ *
5
+ * 4 tools covering: portfolio overview, owner detail, risk status, owner listing.
6
+ * All read-only. Uses server-side aggregated endpoints only (no 600k+ row pulls).
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ // ---------------------------------------------------------------------------
12
+ // Configuration
13
+ // ---------------------------------------------------------------------------
14
+ const BASE = (process.env.TRADING_API_BASE ?? "").replace(/\/$/, "");
15
+ const KEY = process.env.TRADING_API_KEY ?? "";
16
+ if (!BASE || !KEY) {
17
+ process.stderr.write("TRADING_API_BASE and TRADING_API_KEY are required\n");
18
+ process.exit(1);
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // HTTP helper
22
+ // ---------------------------------------------------------------------------
23
+ async function api(path, params) {
24
+ const url = new URL(BASE + path);
25
+ if (params) {
26
+ for (const [k, v] of Object.entries(params)) {
27
+ if (v)
28
+ url.searchParams.set(k, v);
29
+ }
30
+ }
31
+ const res = await fetch(url.toString(), { headers: { "X-API-Key": KEY } });
32
+ if (!res.ok)
33
+ throw new Error(`API ${path} returned ${res.status}`);
34
+ const body = (await res.json());
35
+ return body.data ?? body;
36
+ }
37
+ let ownerCache = null;
38
+ async function getOwnerMap() {
39
+ if (ownerCache)
40
+ return ownerCache;
41
+ const items = (await api("/api/v1/owners", { page_size: "100" }));
42
+ const list = Array.isArray(items)
43
+ ? items
44
+ : (items.items ?? []);
45
+ const idToName = new Map();
46
+ const nameToId = new Map();
47
+ for (const o of list) {
48
+ const id = Number(o.id ?? o.owner_id);
49
+ const name = String(o.user_name ?? o.name ?? o.owner_name ?? "");
50
+ if (id && name) {
51
+ idToName.set(id, name);
52
+ nameToId.set(name, id);
53
+ }
54
+ }
55
+ ownerCache = { idToName, nameToId };
56
+ return ownerCache;
57
+ }
58
+ async function resolveOwnerId(name) {
59
+ const om = await getOwnerMap();
60
+ return om.nameToId.get(name) ?? null;
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Utilities
64
+ // ---------------------------------------------------------------------------
65
+ function fmt(v) {
66
+ if (v == null)
67
+ return null;
68
+ const n = Number(v);
69
+ if (isNaN(n))
70
+ return null;
71
+ return Number.isInteger(n) ? n : parseFloat(n.toFixed(2));
72
+ }
73
+ function text(data) {
74
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Server
78
+ // ---------------------------------------------------------------------------
79
+ const server = new McpServer({ name: "linker-position-mcp", version: "0.1.0" });
80
+ // ═══════════════════════════════════════════════════════════════════════════════
81
+ // Tool 1: get_pnl_overview — 全局盈亏+持仓+VAR 快照
82
+ // ═══════════════════════════════════════════════════════════════════════════════
83
+ server.tool("get_pnl_overview", "查询公司整体盈亏+持仓+VAR快照:公司总已平盈亏、浮动盈亏、总盈亏、VAR总额/剩余/使用率,以及各持仓人的汇总一行(策略数、总盈亏、VAR、预警级别)。适合查询'公司整体什么情况'、'各人盈亏排名'、'谁在赚谁在亏'等场景。", {}, async () => {
84
+ const raw = (await api("/api/v1/pnl/report"));
85
+ const ownerDataList = raw.owner_data ?? [];
86
+ const gt = raw.grand_total;
87
+ // 每人汇总为一行
88
+ const owners = ownerDataList.map((o) => {
89
+ const strategies = o.strategies ?? [];
90
+ const activeStrategies = strategies.filter((s) => {
91
+ const legs = s.legs ?? [];
92
+ return legs.some((l) => Number(l.remaining_quantity ?? 0) > 0);
93
+ });
94
+ const totalPnl = strategies.reduce((sum, s) => sum + Number(s.total_strategy_pnl ?? 0), 0);
95
+ const floatingPnl = strategies.reduce((sum, s) => sum + Number(s.monitoring_pnl ?? 0), 0);
96
+ return {
97
+ owner: o.owner_name,
98
+ active_strategies: activeStrategies.length,
99
+ total_strategies: strategies.length,
100
+ total_pnl: fmt(totalPnl),
101
+ floating_pnl: fmt(floatingPnl),
102
+ var: fmt(o.owner_var),
103
+ var_usage: fmt(o.var_usage_rate),
104
+ alert: o.var_alert_level ?? "normal",
105
+ };
106
+ });
107
+ // 按 total_pnl 降序排
108
+ owners.sort((a, b) => (b.total_pnl ?? 0) - (a.total_pnl ?? 0));
109
+ const result = { owners };
110
+ if (gt) {
111
+ result.company = {
112
+ realized_pnl: fmt(gt.realized_pnl),
113
+ floating_pnl: fmt(gt.floating_pnl),
114
+ total_pnl: fmt(gt.total_pnl),
115
+ var_used: fmt(gt.total_var_used),
116
+ var_remain: fmt(gt.total_var_remain),
117
+ var_limit: gt.total_var_limit_str,
118
+ var_usage: gt.total_usage_rate_str,
119
+ };
120
+ }
121
+ return text(result);
122
+ });
123
+ // ═══════════════════════════════════════════════════════════════════════════════
124
+ // Tool 2: get_owner_detail — 某人持仓+盈亏+VAR 明细
125
+ // ═══════════════════════════════════════════════════════════════════════════════
126
+ server.tool("get_owner_detail", "查询某持仓人的完整持仓+盈亏+VAR明细:该人总盈亏、VAR分项(套保/套利/盘面)、各活跃策略的盈亏和每条持仓腿的合约/方向/数量/均价/市价/浮盈。适合查询'某人持仓什么'、'某人浮盈多少'、'某人各策略盈亏'等场景。", { owner: z.string().describe("持仓人姓名,如 任志华") }, async ({ owner }) => {
127
+ const raw = (await api("/api/v1/pnl/report", {
128
+ owners: owner,
129
+ }));
130
+ const ownerDataList = raw.owner_data ?? [];
131
+ const ownerData = ownerDataList.find((o) => o.owner_name === owner);
132
+ if (!ownerData)
133
+ return text(`未找到持仓人: ${owner}`);
134
+ const strategies = ownerData.strategies ?? [];
135
+ // 只展示有活跃持仓的策略
136
+ const activeStrategies = strategies.filter((s) => {
137
+ const legs = s.legs ?? [];
138
+ return legs.some((l) => Number(l.remaining_quantity ?? 0) > 0);
139
+ });
140
+ const floatingPnl = strategies.reduce((sum, s) => sum + Number(s.monitoring_pnl ?? 0), 0);
141
+ const realizedPart = strategies.reduce((sum, s) => sum + (Number(s.total_strategy_pnl ?? 0) - Number(s.monitoring_pnl ?? 0)), 0);
142
+ const fullyClosedPnl = Number(ownerData.fully_closed_pnl ?? 0);
143
+ const realizedPnl = fullyClosedPnl + realizedPart;
144
+ const totalPnl = realizedPnl + floatingPnl;
145
+ const result = {
146
+ owner,
147
+ realized_pnl: fmt(realizedPnl),
148
+ floating_pnl: fmt(floatingPnl),
149
+ total_pnl: fmt(totalPnl),
150
+ var: {
151
+ total: fmt(ownerData.owner_var),
152
+ hedge: fmt(ownerData.hedge_var),
153
+ arbitrage: fmt(ownerData.arbitrage_var),
154
+ outright: fmt(ownerData.outright_var),
155
+ },
156
+ var_limit: fmt(ownerData.var_limit),
157
+ var_usage_rate: fmt(ownerData.var_usage_rate),
158
+ var_alert: ownerData.var_alert_level,
159
+ active_strategies: activeStrategies.length,
160
+ total_strategies: strategies.length,
161
+ strategies: activeStrategies.map((s) => {
162
+ const legs = (s.legs ?? []).filter((l) => Number(l.remaining_quantity ?? 0) > 0);
163
+ return {
164
+ strategy: s.strategy_name,
165
+ category: s.category,
166
+ product: s.product,
167
+ total_pnl: fmt(s.total_strategy_pnl),
168
+ floating_pnl: fmt(s.monitoring_pnl),
169
+ var: fmt(s.strategy_var),
170
+ legs: legs.map((l) => ({
171
+ contract: l.contract_name,
172
+ direction: l.open_buy_sell,
173
+ remaining_qty: fmt(l.remaining_quantity),
174
+ avg_price: fmt(l.avg_open_price_all),
175
+ market_price: fmt(l.market_price),
176
+ unrealized_pnl: fmt(l.leg_unrealized_pnl),
177
+ realized_pnl: fmt(l.leg_realized_pnl),
178
+ })),
179
+ };
180
+ }),
181
+ };
182
+ return text(result);
183
+ });
184
+ // ═══════════════════════════════════════════════════════════════════════════════
185
+ // Tool 3: get_risk_status — 风险 VAR 维度
186
+ // ═══════════════════════════════════════════════════════════════════════════════
187
+ server.tool("get_risk_status", "查询VAR风险状态。不传owner返回全员VAR概览(各人用量/额度/预警级别+当前超限告警);传owner返回该人近30日VAR历史趋势。适合查询'谁VAR超限了'、'全员风险概览'、'某人VAR趋势'等场景。", { owner: z.string().optional().describe("持仓人姓名(可选)。不传=全员概览,传=某人VAR趋势") }, async ({ owner }) => {
188
+ if (!owner) {
189
+ // 全员 VAR 概览 + 告警
190
+ const [overview, alerts] = await Promise.all([
191
+ api("/api/v1/risk/dashboard/overview"),
192
+ api("/api/v1/risk/dashboard/alerts"),
193
+ ]);
194
+ const overviewList = (Array.isArray(overview) ? overview : []);
195
+ const alertList = (Array.isArray(alerts) ? alerts : []);
196
+ const owners = overviewList.map((o) => ({
197
+ owner: o.owner_name,
198
+ department: o.department,
199
+ var_value: fmt(o.var_value),
200
+ var_limit: fmt(o.var_limit),
201
+ usage_rate: fmt(o.usage_rate),
202
+ alert_level: o.alert_level,
203
+ }));
204
+ // 按 usage_rate 降序
205
+ owners.sort((a, b) => (b.usage_rate ?? 0) - (a.usage_rate ?? 0));
206
+ const result = { owners };
207
+ if (alertList.length > 0) {
208
+ result.alerts = alertList.map((a) => ({
209
+ owner: a.owner_name,
210
+ var_value: fmt(a.var_value),
211
+ var_limit: fmt(a.var_limit),
212
+ usage_rate: fmt(a.usage_rate),
213
+ alert_level: a.alert_level,
214
+ }));
215
+ }
216
+ else {
217
+ result.alerts = "当前无超限告警";
218
+ }
219
+ return text(result);
220
+ }
221
+ else {
222
+ // 某人 VAR 趋势
223
+ const ownerId = await resolveOwnerId(owner);
224
+ if (ownerId == null)
225
+ return text(`未找到持仓人: ${owner}`);
226
+ const history = (await api("/api/v1/risk/dashboard/history", {
227
+ owner_id: String(ownerId),
228
+ days: "30",
229
+ }));
230
+ const points = (Array.isArray(history) ? history : []);
231
+ if (!points.length)
232
+ return text(`${owner} 近30日无VAR记录`);
233
+ return text({
234
+ owner,
235
+ days: 30,
236
+ trend: points.map((p) => ({
237
+ date: p.calc_date,
238
+ var_value: fmt(p.var_value),
239
+ var_limit: fmt(p.var_limit),
240
+ usage_rate: fmt(p.usage_rate),
241
+ alert_level: p.alert_level,
242
+ })),
243
+ });
244
+ }
245
+ });
246
+ // ═══════════════════════════════════════════════════════════════════════════════
247
+ // Tool 4: list_owners — 持仓人列表(辅助)
248
+ // ═══════════════════════════════════════════════════════════════════════════════
249
+ server.tool("list_owners", "列出系统中所有持仓人姓名。适合在查询某人持仓/盈亏/VAR前先确认姓名是否存在,或用户问'有哪些交易员'时使用。", {}, async () => {
250
+ const om = await getOwnerMap();
251
+ const names = Array.from(om.nameToId.keys()).sort();
252
+ return text(names);
253
+ });
254
+ // ---------------------------------------------------------------------------
255
+ // Start
256
+ // ---------------------------------------------------------------------------
257
+ const transport = new StdioServerTransport();
258
+ await server.connect(transport);
259
+ process.stderr.write("[linker-position-mcp] running on stdio\n");
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "linker-position-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Linker Position & PnL queries - portfolio overview, owner detail, risk status",
5
+ "type": "module",
6
+ "bin": {
7
+ "linker-position-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc && shx chmod +x dist/*.js",
14
+ "prepare": "npm run build",
15
+ "watch": "tsc --watch"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.26.0",
19
+ "zod": "^3.24.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22",
23
+ "shx": "^0.3.4",
24
+ "typescript": "^5.8.2"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "license": "MIT"
30
+ }