aqc-mcp 2.0.0 → 2.0.1
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 +1 -1
- package/src/tools/esasky.ts +26 -11
- package/src/tools/heasarc.ts +23 -15
- package/src/tools/mast.ts +3 -7
- package/src/utils/http.ts +33 -1
package/package.json
CHANGED
package/src/tools/esasky.ts
CHANGED
|
@@ -3,6 +3,24 @@ import { tapQuery, formatTapResult } from '../utils/http.js';
|
|
|
3
3
|
|
|
4
4
|
const ESASKY_TAP = 'https://sky.esa.int/esasky-tap/tap/sync';
|
|
5
5
|
|
|
6
|
+
const MISSION_TABLES: Record<string, string> = {
|
|
7
|
+
'xmm': 'observations.mv_v_v_xsa_esasky_photo_fdw_fdw',
|
|
8
|
+
'hst': 'observations.mv_v_v_hst_mmi_observation_fdw_fdw',
|
|
9
|
+
'alma': 'observations.mv_v_v_alma_obs_fdw',
|
|
10
|
+
'jwst': 'observations.mv_v_jwst_obs_fdw',
|
|
11
|
+
'chandra': 'observations.mv_chandra_obs_photo_fdw',
|
|
12
|
+
'herschel': 'observations.mv_v_v_hsa_esasky_photo_fdw_fdw',
|
|
13
|
+
'spitzer': 'observations.mv_spitzer_irac_fdw',
|
|
14
|
+
'suzaku': 'observations.mv_suzaku_data_fdw',
|
|
15
|
+
'cheops': 'observations.mv_cheops_obs_fdw',
|
|
16
|
+
'xmm-om': 'observations.mv_v_esasky_xmm_om_uv_fdw',
|
|
17
|
+
'iso': 'observations.mv_iso_spectra_fdw',
|
|
18
|
+
'iue': 'observations.mv_iue_spectra_fdw',
|
|
19
|
+
'akari': 'observations.mv_akari_irc_fdw',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_MISSIONS = Object.keys(MISSION_TABLES).join(', ');
|
|
23
|
+
|
|
6
24
|
export function registerEsaskyTools(server: any) {
|
|
7
25
|
server.tool(
|
|
8
26
|
'esasky_query',
|
|
@@ -11,23 +29,20 @@ export function registerEsaskyTools(server: any) {
|
|
|
11
29
|
ra: z.number().describe('Right Ascension in degrees (0-360)'),
|
|
12
30
|
dec: z.number().describe('Declination in degrees (-90 to 90)'),
|
|
13
31
|
radius: z.number().default(10).describe('Search radius in arcminutes'),
|
|
14
|
-
mission: z.string().default('
|
|
32
|
+
mission: z.string().default('xmm').describe(`Mission name: ${SUPPORTED_MISSIONS}, or "all" for XMM default`),
|
|
15
33
|
max_results: z.number().default(50).describe('Maximum number of results'),
|
|
16
34
|
lang: z.enum(['en', 'zh']).default('en').describe('Output language'),
|
|
17
35
|
},
|
|
18
36
|
async ({ ra, dec, radius, mission, max_results, lang }: { ra: number; dec: number; radius: number; mission: string; max_results: number; lang: string }) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
let missionFilter = '';
|
|
37
|
+
const missionKey = mission.toLowerCase();
|
|
38
|
+
const tableName = MISSION_TABLES[missionKey] ?? MISSION_TABLES['xmm'];
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const adql = `SELECT TOP ${max_results} obs_id, obs_collection, target_name, s_ra, s_dec, instrument_name, t_exptime, access_url FROM ${tableName} WHERE CONTAINS(POINT('ICRS', s_ra, s_dec), CIRCLE('ICRS', ${ra}, ${dec}, ${radiusDeg})) = 1${missionFilter}`;
|
|
40
|
+
const radiusDeg = radius / 60;
|
|
41
|
+
const adql = `SELECT TOP ${max_results} observation_id, ra_deg, dec_deg, target_name, start_time, end_time FROM ${tableName} WHERE CONTAINS(POINT('ICRS', ra_deg, dec_deg), CIRCLE('ICRS', ${ra}, ${dec}, ${radiusDeg})) = 1`;
|
|
28
42
|
|
|
29
|
-
const result = await tapQuery({ endpoint: ESASKY_TAP, adql, maxrec: max_results });
|
|
30
|
-
const
|
|
43
|
+
const result = await tapQuery({ endpoint: ESASKY_TAP, adql, format: 'csv', maxrec: max_results });
|
|
44
|
+
const resolvedMission = MISSION_TABLES[missionKey] ? missionKey.toUpperCase() : 'XMM';
|
|
45
|
+
const title = lang === 'zh' ? `ESASky 查询结果: (${ra}, ${dec}) [${resolvedMission}]` : `ESASky Query Result: (${ra}, ${dec}) [${resolvedMission}]`;
|
|
31
46
|
return { content: [{ type: 'text' as const, text: formatTapResult(result, title) }] };
|
|
32
47
|
}
|
|
33
48
|
);
|
package/src/tools/heasarc.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { z } from 'zod';
|
|
|
2
2
|
import { tapQuery, formatTapResult } from '../utils/http.js';
|
|
3
3
|
|
|
4
4
|
const HEASARC_TAP = 'https://heasarc.gsfc.nasa.gov/xamin/vo/tap/sync';
|
|
5
|
+
const SIMBAD_TAP = 'https://simbad.cds.unistra.fr/simbad/sim-tap/sync';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve an object name to RA/Dec via SIMBAD TAP (supports JSON).
|
|
9
|
+
*/
|
|
10
|
+
async function resolveCoords(name: string): Promise<{ ra: number; dec: number }> {
|
|
11
|
+
const safeName = name.replace(/'/g, "''");
|
|
12
|
+
const adql = `SELECT ra, dec FROM basic JOIN ident ON oidref = oid WHERE id = '${safeName}'`;
|
|
13
|
+
const result = await tapQuery({ endpoint: SIMBAD_TAP, adql, format: 'json' });
|
|
14
|
+
if (result.rows.length === 0) {
|
|
15
|
+
throw new Error(`Could not resolve object name: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
const row = result.rows[0];
|
|
18
|
+
return { ra: Number(row.ra), dec: Number(row.dec) };
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
export function registerHeasarcTools(server: any) {
|
|
7
22
|
server.tool(
|
|
@@ -15,23 +30,16 @@ export function registerHeasarcTools(server: any) {
|
|
|
15
30
|
lang: z.enum(['en', 'zh']).default('en').describe('Output language'),
|
|
16
31
|
},
|
|
17
32
|
async ({ object_name, mission, radius, max_results, lang }: { object_name: string; mission: string; radius: number; max_results: number; lang: string }) => {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
// Step 1: Resolve object name to coordinates via SIMBAD
|
|
34
|
+
const coords = await resolveCoords(object_name);
|
|
35
|
+
|
|
36
|
+
// Step 2: Cone search on HEASARC with FORMAT=text (only format that works)
|
|
21
37
|
const radiusDeg = radius / 60;
|
|
22
|
-
const adql = `SELECT TOP ${max_results} * FROM ${mission} WHERE CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS',
|
|
38
|
+
const adql = `SELECT TOP ${max_results} * FROM ${mission} WHERE CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', ${coords.ra}, ${coords.dec}, ${radiusDeg})) = 1`;
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return { content: [{ type: 'text' as const, text: formatTapResult(result, title) }] };
|
|
28
|
-
} catch {
|
|
29
|
-
// Fallback: some HEASARC tables use name column directly
|
|
30
|
-
const fallbackAdql = `SELECT TOP ${max_results} * FROM ${mission} WHERE name LIKE '%${object_name.replace(/'/g, "''").replace(/%/g, '')}%'`;
|
|
31
|
-
const result = await tapQuery({ endpoint: HEASARC_TAP, adql: fallbackAdql, maxrec: max_results });
|
|
32
|
-
const title = lang === 'zh' ? `HEASARC 查询结果: ${object_name} (${mission})` : `HEASARC Query Result: ${object_name} (${mission})`;
|
|
33
|
-
return { content: [{ type: 'text' as const, text: formatTapResult(result, title) }] };
|
|
34
|
-
}
|
|
40
|
+
const result = await tapQuery({ endpoint: HEASARC_TAP, adql, format: 'text', maxrec: max_results });
|
|
41
|
+
const title = lang === 'zh' ? `HEASARC 查询结果: ${object_name} (${mission})` : `HEASARC Query Result: ${object_name} (${mission})`;
|
|
42
|
+
return { content: [{ type: 'text' as const, text: formatTapResult(result, title) }] };
|
|
35
43
|
}
|
|
36
44
|
);
|
|
37
45
|
}
|
package/src/tools/mast.ts
CHANGED
|
@@ -3,24 +3,20 @@ import { z } from 'zod';
|
|
|
3
3
|
const MAST_API = 'https://mast.stsci.edu/api/v0/invoke';
|
|
4
4
|
|
|
5
5
|
async function mastQuery(service: string, params: Record<string, unknown>, timeout = 60000): Promise<unknown> {
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const requestPayload = JSON.stringify({ service, params, format: 'json', timeout: Math.floor(timeout / 1000) });
|
|
8
7
|
const controller = new AbortController();
|
|
9
8
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
10
|
-
|
|
11
9
|
try {
|
|
12
10
|
const response = await fetch(MAST_API, {
|
|
13
11
|
method: 'POST',
|
|
14
|
-
headers: { 'Content-Type': 'application/
|
|
15
|
-
body
|
|
12
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
13
|
+
body: `request=${encodeURIComponent(requestPayload)}`,
|
|
16
14
|
signal: controller.signal,
|
|
17
15
|
});
|
|
18
|
-
|
|
19
16
|
if (!response.ok) {
|
|
20
17
|
const text = await response.text().catch(() => '');
|
|
21
18
|
throw new Error(`MAST API error (${response.status}): ${text.slice(0, 500)}`);
|
|
22
19
|
}
|
|
23
|
-
|
|
24
20
|
return await response.json();
|
|
25
21
|
} finally {
|
|
26
22
|
clearTimeout(timer);
|
package/src/utils/http.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export interface TapQueryOptions {
|
|
8
8
|
endpoint: string;
|
|
9
9
|
adql: string;
|
|
10
|
-
format?: 'json' | 'csv' | 'votable';
|
|
10
|
+
format?: 'json' | 'csv' | 'votable' | 'text';
|
|
11
11
|
timeout?: number;
|
|
12
12
|
maxrec?: number;
|
|
13
13
|
}
|
|
@@ -68,10 +68,42 @@ function parseTapResponse(text: string, format: string): TapResult {
|
|
|
68
68
|
if (format === 'csv') {
|
|
69
69
|
return parseCsvResponse(text);
|
|
70
70
|
}
|
|
71
|
+
if (format === 'text') {
|
|
72
|
+
return parseTextResponse(text);
|
|
73
|
+
}
|
|
71
74
|
// votable — return raw text as single-row result
|
|
72
75
|
return { columns: ['votable'], rows: [{ votable: text }] };
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Parse pipe-delimited text response from HEASARC TAP.
|
|
80
|
+
* Format: header row with | separators, data rows, footer with 'Number of rows/columns'.
|
|
81
|
+
*/
|
|
82
|
+
function parseTextResponse(text: string): TapResult {
|
|
83
|
+
const lines = text.split('\n').filter(line => {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
return trimmed.length > 0 && !trimmed.startsWith('Number of');
|
|
86
|
+
});
|
|
87
|
+
if (lines.length === 0) {
|
|
88
|
+
return { columns: [], rows: [] };
|
|
89
|
+
}
|
|
90
|
+
const headers = lines[0].split('|').map(h => h.trim()).filter(h => h.length > 0);
|
|
91
|
+
const rows: Record<string, unknown>[] = [];
|
|
92
|
+
for (let i = 1; i < lines.length; i++) {
|
|
93
|
+
const values = lines[i].split('|').map(v => v.trim());
|
|
94
|
+
// Skip separator lines (e.g. '---+---+---')
|
|
95
|
+
if (values.every(v => /^[-+]+$/.test(v) || v.length === 0)) continue;
|
|
96
|
+
const obj: Record<string, unknown> = {};
|
|
97
|
+
for (let j = 0; j < headers.length; j++) {
|
|
98
|
+
const val = values[j + (values.length > headers.length ? 1 : 0)] ?? '';
|
|
99
|
+
const num = Number(val);
|
|
100
|
+
obj[headers[j]] = val.length > 0 && !isNaN(num) && val !== '' ? num : val;
|
|
101
|
+
}
|
|
102
|
+
rows.push(obj);
|
|
103
|
+
}
|
|
104
|
+
return { columns: headers, rows };
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
function parseJsonResponse(text: string): TapResult {
|
|
76
108
|
const data = JSON.parse(text);
|
|
77
109
|
|