@lightcone-ai/daemon 0.9.78 → 0.10.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 (43) hide show
  1. package/mcp-servers/mysql/index.js +13 -5
  2. package/mcp-servers/mysql/manifest.json +16 -0
  3. package/mcp-servers/official/company-fundamentals/index.js +34 -0
  4. package/mcp-servers/official/company-fundamentals/manifest.json +14 -0
  5. package/mcp-servers/official/compliance-check/index.js +49 -0
  6. package/mcp-servers/official/compliance-check/manifest.json +14 -0
  7. package/mcp-servers/official/industry-report/index.js +34 -0
  8. package/mcp-servers/official/industry-report/manifest.json +14 -0
  9. package/mcp-servers/official/market-data-query/index.js +34 -0
  10. package/mcp-servers/official/market-data-query/manifest.json +14 -0
  11. package/mcp-servers/official/portfolio-analysis/index.js +74 -0
  12. package/mcp-servers/official/portfolio-analysis/manifest.json +14 -0
  13. package/mcp-servers/official/portfolio-read/index.js +34 -0
  14. package/mcp-servers/official/portfolio-read/manifest.json +14 -0
  15. package/mcp-servers/official/research-fetch/index.js +35 -0
  16. package/mcp-servers/official/research-fetch/manifest.json +14 -0
  17. package/mcp-servers/official/risk-metrics/index.js +34 -0
  18. package/mcp-servers/official/risk-metrics/manifest.json +14 -0
  19. package/mcp-servers/official-common/fixtures.js +273 -0
  20. package/mcp-servers/official-common/server.js +34 -0
  21. package/mcp-servers/platform/manifest.json +15 -0
  22. package/mcp-servers/portfolio-analysis/core.js +592 -0
  23. package/mcp-servers/portfolio-analysis/index.js +45 -0
  24. package/mcp-servers/portfolio-analysis/package-lock.json +1139 -0
  25. package/mcp-servers/portfolio-analysis/package.json +10 -0
  26. package/mcp-servers/portfolio-read/core.js +330 -0
  27. package/mcp-servers/portfolio-read/index.js +127 -0
  28. package/mcp-servers/portfolio-read/package-lock.json +1243 -0
  29. package/mcp-servers/portfolio-read/package.json +11 -0
  30. package/mcp-servers/publisher/adapters/douyin.js +112 -20
  31. package/mcp-servers/publisher/index.js +84 -8
  32. package/mcp-servers/publisher/manifest.json +16 -0
  33. package/package.json +1 -1
  34. package/src/agent-manager.js +761 -187
  35. package/src/chat-bridge.js +567 -92
  36. package/src/connection.js +1 -1
  37. package/src/drivers/claude.js +48 -45
  38. package/src/drivers/codex.js +110 -7
  39. package/src/drivers/kimi.js +80 -34
  40. package/src/governance-state.js +89 -0
  41. package/src/index.js +34 -16
  42. package/src/lease-window.js +8 -0
  43. package/src/mcp-config.js +52 -21
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "portfolio-analysis-mcp-server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "@modelcontextprotocol/sdk": "^1.29.0",
8
+ "zod": "^4.3.6"
9
+ }
10
+ }
@@ -0,0 +1,330 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ const DEFAULT_CURRENCY = 'CNY';
4
+ const DEFAULT_SOURCE_RISK = 'normal';
5
+
6
+ const HEADER_ALIASES = Object.freeze({
7
+ symbol: ['symbol', 'ticker', 'code', 'sec_code', 'security_code', '股票代码', '证券代码', '代码'],
8
+ name: ['name', 'asset_name', 'security_name', 'instrument_name', '股票名称', '证券名称', '名称'],
9
+ quantity: ['quantity', 'qty', 'shares', 'position', 'holding', '持仓', '持仓数量', '数量', '股数'],
10
+ avgCost: ['avg_cost', 'average_cost', 'cost', 'cost_price', 'avg_price', '买入均价', '成本价', '平均成本', '成本'],
11
+ marketPrice: ['market_price', 'price', 'last_price', 'close', '最新价', '现价', '市价'],
12
+ marketValue: ['market_value', 'value', 'position_value', 'market_cap', '市值', '持仓市值'],
13
+ costBasis: ['cost_basis', 'total_cost', 'position_cost', '成本市值', '持仓成本'],
14
+ sector: ['sector', 'industry', '行业', '板块'],
15
+ assetClass: ['asset_class', 'asset_type', '类别', '资产类别'],
16
+ currency: ['currency', 'ccy', '币种'],
17
+ });
18
+
19
+ function normalizeHeader(value) {
20
+ return String(value ?? '')
21
+ .replace(/^\uFEFF/, '')
22
+ .trim()
23
+ .toLowerCase()
24
+ .replace(/\s+/g, '_')
25
+ .replace(/[-/]+/g, '_');
26
+ }
27
+
28
+ function normalizeCell(value) {
29
+ return String(value ?? '').trim();
30
+ }
31
+
32
+ function sanitizeSymbol(value) {
33
+ const normalized = String(value ?? '').trim();
34
+ if (!normalized) return '';
35
+ return normalized.toUpperCase();
36
+ }
37
+
38
+ function toNumber(value) {
39
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
40
+ const raw = normalizeCell(value);
41
+ if (!raw) return null;
42
+ const cleaned = raw
43
+ .replace(/,/g, '')
44
+ .replace(/\$/g, '')
45
+ .replace(/¥/g, '')
46
+ .replace(/%/g, '%');
47
+ const percent = cleaned.endsWith('%');
48
+ const numeric = Number(percent ? cleaned.slice(0, -1) : cleaned);
49
+ if (!Number.isFinite(numeric)) return null;
50
+ return percent ? numeric / 100 : numeric;
51
+ }
52
+
53
+ function round(value, digits = 6) {
54
+ if (!Number.isFinite(value)) return null;
55
+ const factor = 10 ** digits;
56
+ return Math.round(value * factor) / factor;
57
+ }
58
+
59
+ function countDelimiterOutsideQuotes(line, delimiter) {
60
+ let count = 0;
61
+ let inQuotes = false;
62
+ for (let i = 0; i < line.length; i += 1) {
63
+ const ch = line[i];
64
+ if (ch === '"') {
65
+ if (inQuotes && line[i + 1] === '"') {
66
+ i += 1;
67
+ } else {
68
+ inQuotes = !inQuotes;
69
+ }
70
+ continue;
71
+ }
72
+ if (!inQuotes && ch === delimiter) count += 1;
73
+ }
74
+ return count;
75
+ }
76
+
77
+ function detectDelimiter(csvText) {
78
+ const firstLine = String(csvText ?? '').split(/\r?\n/).find(line => line.trim().length > 0) ?? '';
79
+ const candidates = [',', '\t', ';', '|'];
80
+ let best = ',';
81
+ let bestCount = -1;
82
+ for (const candidate of candidates) {
83
+ const count = countDelimiterOutsideQuotes(firstLine, candidate);
84
+ if (count > bestCount) {
85
+ best = candidate;
86
+ bestCount = count;
87
+ }
88
+ }
89
+ return best;
90
+ }
91
+
92
+ function parseDelimitedRows(csvText, delimiter) {
93
+ const rows = [];
94
+ let row = [];
95
+ let cell = '';
96
+ let inQuotes = false;
97
+
98
+ for (let i = 0; i < csvText.length; i += 1) {
99
+ const ch = csvText[i];
100
+
101
+ if (ch === '"') {
102
+ if (inQuotes && csvText[i + 1] === '"') {
103
+ cell += '"';
104
+ i += 1;
105
+ } else {
106
+ inQuotes = !inQuotes;
107
+ }
108
+ continue;
109
+ }
110
+
111
+ if (!inQuotes && ch === delimiter) {
112
+ row.push(cell);
113
+ cell = '';
114
+ continue;
115
+ }
116
+
117
+ if (!inQuotes && (ch === '\n' || ch === '\r')) {
118
+ if (ch === '\r' && csvText[i + 1] === '\n') i += 1;
119
+ row.push(cell);
120
+ const hasData = row.some(item => normalizeCell(item).length > 0);
121
+ if (hasData) rows.push(row);
122
+ row = [];
123
+ cell = '';
124
+ continue;
125
+ }
126
+
127
+ cell += ch;
128
+ }
129
+
130
+ if (cell.length > 0 || row.length > 0) {
131
+ row.push(cell);
132
+ const hasData = row.some(item => normalizeCell(item).length > 0);
133
+ if (hasData) rows.push(row);
134
+ }
135
+
136
+ return rows;
137
+ }
138
+
139
+ function lookupValue(row, aliases) {
140
+ for (const alias of aliases) {
141
+ const normalized = normalizeHeader(alias);
142
+ if (Object.prototype.hasOwnProperty.call(row, normalized)) {
143
+ const value = row[normalized];
144
+ if (normalizeCell(value)) return value;
145
+ }
146
+ }
147
+ return '';
148
+ }
149
+
150
+ function normalizeRow(rawRow, index, { defaultCurrency }) {
151
+ const symbol = sanitizeSymbol(lookupValue(rawRow, HEADER_ALIASES.symbol));
152
+ if (!symbol) {
153
+ return {
154
+ holding: null,
155
+ warning: `row_${index + 1}: missing symbol/code, skipped`,
156
+ };
157
+ }
158
+
159
+ const quantity = toNumber(lookupValue(rawRow, HEADER_ALIASES.quantity));
160
+ const avgCost = toNumber(lookupValue(rawRow, HEADER_ALIASES.avgCost));
161
+ const marketPrice = toNumber(lookupValue(rawRow, HEADER_ALIASES.marketPrice));
162
+ let marketValue = toNumber(lookupValue(rawRow, HEADER_ALIASES.marketValue));
163
+ let costBasis = toNumber(lookupValue(rawRow, HEADER_ALIASES.costBasis));
164
+
165
+ if (marketValue == null && quantity != null && marketPrice != null) {
166
+ marketValue = quantity * marketPrice;
167
+ }
168
+ if (costBasis == null && quantity != null && avgCost != null) {
169
+ costBasis = quantity * avgCost;
170
+ }
171
+
172
+ const holding = {
173
+ symbol,
174
+ name: normalizeCell(lookupValue(rawRow, HEADER_ALIASES.name)) || symbol,
175
+ quantity: round(quantity, 6),
176
+ avg_cost: round(avgCost, 6),
177
+ market_price: round(marketPrice, 6),
178
+ market_value: round(marketValue, 2),
179
+ cost_basis: round(costBasis, 2),
180
+ sector: normalizeCell(lookupValue(rawRow, HEADER_ALIASES.sector)) || 'Unknown',
181
+ asset_class: normalizeCell(lookupValue(rawRow, HEADER_ALIASES.assetClass)) || 'equity',
182
+ currency: normalizeCell(lookupValue(rawRow, HEADER_ALIASES.currency)) || defaultCurrency,
183
+ };
184
+
185
+ return { holding, warning: null };
186
+ }
187
+
188
+ function summarizeHoldings(holdings) {
189
+ let totalMarketValue = 0;
190
+ let totalCostBasis = 0;
191
+ let missingMarketValue = 0;
192
+ let missingCostBasis = 0;
193
+
194
+ for (const holding of holdings) {
195
+ if (holding.market_value == null) {
196
+ missingMarketValue += 1;
197
+ } else {
198
+ totalMarketValue += holding.market_value;
199
+ }
200
+
201
+ if (holding.cost_basis == null) {
202
+ missingCostBasis += 1;
203
+ } else {
204
+ totalCostBasis += holding.cost_basis;
205
+ }
206
+ }
207
+
208
+ const hasPnl = holdings.length > 0 && missingMarketValue === 0 && missingCostBasis === 0;
209
+
210
+ return {
211
+ position_count: holdings.length,
212
+ total_market_value: round(totalMarketValue, 2),
213
+ total_cost_basis: round(totalCostBasis, 2),
214
+ unrealized_pnl: hasPnl ? round(totalMarketValue - totalCostBasis, 2) : null,
215
+ missing_market_value_count: missingMarketValue,
216
+ missing_cost_basis_count: missingCostBasis,
217
+ };
218
+ }
219
+
220
+ function rowsToObjects(rows) {
221
+ if (rows.length === 0) return [];
222
+ const headerRow = rows[0].map(item => normalizeHeader(item));
223
+ const objects = [];
224
+
225
+ for (let i = 1; i < rows.length; i += 1) {
226
+ const raw = rows[i];
227
+ const row = {};
228
+ for (let col = 0; col < headerRow.length; col += 1) {
229
+ const header = headerRow[col];
230
+ if (!header) continue;
231
+ row[header] = normalizeCell(raw[col] ?? '');
232
+ }
233
+ objects.push(row);
234
+ }
235
+
236
+ return objects;
237
+ }
238
+
239
+ export function parsePortfolioCsv(csvText, options = {}) {
240
+ const content = String(csvText ?? '').replace(/^\uFEFF/, '').trim();
241
+ if (!content) throw new Error('CSV content is empty');
242
+
243
+ const delimiter = options.delimiter || detectDelimiter(content);
244
+ const rows = parseDelimitedRows(content, delimiter);
245
+ if (rows.length < 2) {
246
+ throw new Error('CSV requires a header row and at least one data row');
247
+ }
248
+
249
+ const objects = rowsToObjects(rows);
250
+ const holdings = [];
251
+ const warnings = [];
252
+
253
+ for (let i = 0; i < objects.length; i += 1) {
254
+ const result = normalizeRow(objects[i], i, {
255
+ defaultCurrency: options.currency || DEFAULT_CURRENCY,
256
+ });
257
+ if (result.warning) warnings.push(result.warning);
258
+ if (result.holding) holdings.push(result.holding);
259
+ }
260
+
261
+ if (holdings.length === 0) {
262
+ throw new Error('No valid holdings parsed from CSV');
263
+ }
264
+
265
+ return {
266
+ source: 'csv',
267
+ source_name: options.sourceName || 'user_upload_csv',
268
+ source_risk: DEFAULT_SOURCE_RISK,
269
+ as_of: options.asOf || new Date().toISOString(),
270
+ currency: options.currency || DEFAULT_CURRENCY,
271
+ holdings,
272
+ summary: summarizeHoldings(holdings),
273
+ warnings,
274
+ };
275
+ }
276
+
277
+ export function normalizePortfolioSnapshot(snapshot, options = {}) {
278
+ const payload = snapshot && typeof snapshot === 'object' ? snapshot : {};
279
+ const rows = Array.isArray(payload.holdings)
280
+ ? payload.holdings
281
+ : (Array.isArray(payload.positions) ? payload.positions : []);
282
+
283
+ if (rows.length === 0) {
284
+ throw new Error('Broker response does not include holdings/positions array');
285
+ }
286
+
287
+ const normalizedRows = rows.map(row => {
288
+ const normalized = {};
289
+ for (const [key, value] of Object.entries(row ?? {})) {
290
+ normalized[normalizeHeader(key)] = value;
291
+ }
292
+ return normalized;
293
+ });
294
+
295
+ const holdings = [];
296
+ const warnings = [];
297
+ for (let i = 0; i < normalizedRows.length; i += 1) {
298
+ const result = normalizeRow(normalizedRows[i], i, {
299
+ defaultCurrency: options.currency || payload.currency || DEFAULT_CURRENCY,
300
+ });
301
+ if (result.warning) warnings.push(result.warning);
302
+ if (result.holding) holdings.push(result.holding);
303
+ }
304
+
305
+ if (holdings.length === 0) {
306
+ throw new Error('Broker response has no valid holdings after normalization');
307
+ }
308
+
309
+ return {
310
+ source: options.source || 'broker_api',
311
+ source_name: options.sourceName || 'broker_snapshot',
312
+ source_risk: DEFAULT_SOURCE_RISK,
313
+ as_of: options.asOf || payload.as_of || new Date().toISOString(),
314
+ currency: options.currency || payload.currency || DEFAULT_CURRENCY,
315
+ holdings,
316
+ summary: summarizeHoldings(holdings),
317
+ warnings,
318
+ };
319
+ }
320
+
321
+ export function readCsvInput({ csvContent, csvBase64, filePath }) {
322
+ if (typeof csvContent === 'string' && csvContent.trim()) return csvContent;
323
+ if (typeof csvBase64 === 'string' && csvBase64.trim()) {
324
+ return Buffer.from(csvBase64, 'base64').toString('utf8');
325
+ }
326
+ if (typeof filePath === 'string' && filePath.trim()) {
327
+ return readFileSync(filePath, 'utf8');
328
+ }
329
+ throw new Error('Provide one of: csv_content, csv_base64, file_path');
330
+ }
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { extname } from 'path';
6
+
7
+ import {
8
+ parsePortfolioCsv,
9
+ normalizePortfolioSnapshot,
10
+ readCsvInput,
11
+ } from './core.js';
12
+
13
+ const BROKER_BASE_URL = process.env.PORTFOLIO_BROKER_BASE_URL ?? '';
14
+ const BROKER_TOKEN = process.env.PORTFOLIO_BROKER_TOKEN ?? '';
15
+ const EXCEL_EXTENSIONS = new Set(['.xlsx', '.xls', '.xlsm', '.xlsb']);
16
+
17
+ function toolError(message) {
18
+ return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] };
19
+ }
20
+
21
+ async function readPortfolioFromBroker({ broker_account_id, as_of, currency }) {
22
+ if (!BROKER_BASE_URL || !BROKER_TOKEN) {
23
+ throw new Error('broker credentials unavailable: ensure Secret Broker injects PORTFOLIO_BROKER_BASE_URL and PORTFOLIO_BROKER_TOKEN');
24
+ }
25
+
26
+ const url = new URL('/v1/portfolio/snapshot', BROKER_BASE_URL);
27
+ if (broker_account_id) url.searchParams.set('account_id', broker_account_id);
28
+
29
+ const res = await fetch(url, {
30
+ method: 'GET',
31
+ headers: {
32
+ 'Authorization': `Bearer ${BROKER_TOKEN}`,
33
+ 'Accept': 'application/json',
34
+ },
35
+ });
36
+
37
+ if (!res.ok) {
38
+ throw new Error(`broker request failed with status ${res.status}`);
39
+ }
40
+
41
+ const payload = await res.json();
42
+ return normalizePortfolioSnapshot(payload, {
43
+ source: 'broker_api',
44
+ sourceName: 'broker_snapshot',
45
+ asOf: as_of,
46
+ currency,
47
+ });
48
+ }
49
+
50
+ async function readTabularInput(input) {
51
+ const filePath = typeof input.file_path === 'string' ? input.file_path.trim() : '';
52
+ if (filePath && EXCEL_EXTENSIONS.has(extname(filePath).toLowerCase())) {
53
+ let xlsxModule;
54
+ try {
55
+ xlsxModule = await import('xlsx');
56
+ } catch {
57
+ throw new Error('Excel parsing requires the xlsx dependency. Install dependencies or convert the file to CSV.');
58
+ }
59
+
60
+ const xlsx = xlsxModule?.default ?? xlsxModule;
61
+ const workbook = xlsx.readFile(filePath, { cellDates: false });
62
+ const firstSheet = workbook.SheetNames?.[0];
63
+ if (!firstSheet) {
64
+ throw new Error('Excel file has no worksheet');
65
+ }
66
+
67
+ const csv = xlsx.utils.sheet_to_csv(workbook.Sheets[firstSheet], { blankrows: false });
68
+ if (!csv.trim()) {
69
+ throw new Error('Excel worksheet is empty');
70
+ }
71
+ return csv;
72
+ }
73
+
74
+ return readCsvInput({
75
+ csvContent: input.csv_content,
76
+ csvBase64: input.csv_base64,
77
+ filePath: input.file_path,
78
+ });
79
+ }
80
+
81
+ const server = new McpServer({ name: 'portfolio-read', version: '0.1.0' });
82
+
83
+ server.tool(
84
+ 'portfolio_read',
85
+ 'Read user portfolio holdings. Supports CSV and Excel (xlsx/xls) input; broker API mode is available only with Secret Broker injected credentials.',
86
+ {
87
+ source_type: z.enum(['csv', 'broker']).optional().describe('Data source type. Default is csv.'),
88
+ csv_content: z.string().optional().describe('Raw CSV content.'),
89
+ csv_base64: z.string().optional().describe('Base64-encoded CSV content.'),
90
+ file_path: z.string().optional().describe('Absolute local file path of CSV or Excel (xlsx/xls) input.'),
91
+ delimiter: z.string().optional().describe('Optional delimiter override, e.g. "," or "\\t".'),
92
+ source_name: z.string().optional().describe('Logical source label for audit/reporting.'),
93
+ broker_account_id: z.string().optional().describe('Broker account identifier (required in broker mode when API needs it).'),
94
+ currency: z.string().optional().describe('Portfolio currency, default CNY.'),
95
+ as_of: z.string().optional().describe('As-of timestamp override (ISO8601 recommended).'),
96
+ },
97
+ async (input) => {
98
+ try {
99
+ const sourceType = input.source_type ?? 'csv';
100
+
101
+ const report = sourceType === 'broker'
102
+ ? await readPortfolioFromBroker(input)
103
+ : parsePortfolioCsv(
104
+ await readTabularInput(input),
105
+ {
106
+ delimiter: input.delimiter,
107
+ sourceName: input.source_name,
108
+ currency: input.currency,
109
+ asOf: input.as_of,
110
+ }
111
+ );
112
+
113
+ return {
114
+ content: [{
115
+ type: 'text',
116
+ text: JSON.stringify(report, null, 2),
117
+ }],
118
+ };
119
+ } catch (error) {
120
+ return toolError(error.message);
121
+ }
122
+ }
123
+ );
124
+
125
+ const transport = new StdioServerTransport();
126
+ await server.connect(transport);
127
+ console.error('[portfolio-read] MCP Server started');