get-tickets
Query real-time train ticket availability on 12306 by date and stations. Filter by train type, time range, and sort results. Get output in text, CSV, or JSON.
Instructions
查询12306余票信息。
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| date | Yes | 查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。 | |
| fromStation | Yes | 出发地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到) | |
| toStation | Yes | 到达地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到) | |
| trainFilterFlags | No | 车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 "G"。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)] | |
| earliestStartTime | No | 最早出发时间(0-24),默认为0。 | |
| latestStartTime | No | 最迟出发时间(0-24),默认为24。 | |
| sortFlag | No | 排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)] | |
| sortReverse | No | 是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。 | |
| limitedNum | No | 返回的余票数量限制,默认为0,即不限制。 | |
| format | No | 返回结果格式,默认为text,建议使用text与csv。可选标志:[text, csv, json] | text |
Implementation Reference
- src/index.ts:954-1138 (registration)Tool registration for 'get-tickets' via server.tool(), defining the MCP tool name, description, Zod schema for inputs, and the async handler.
server.tool( 'get-tickets', '查询12306余票信息。', { date: z .string() .length(10) .describe( '查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。' ), fromStation: z .string() .describe( '出发地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到)' ), toStation: z .string() .describe( '到达地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到)' ), trainFilterFlags: z .string() .regex(/^[GDZTKOFS]*$/) .max(8) .optional() .default('') .describe( '车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 "G"。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]' ), earliestStartTime: z .number() .min(0) .max(24) .optional() .default(0) .describe('最早出发时间(0-24),默认为0。'), latestStartTime: z .number() .min(0) .max(24) .optional() .default(24) .describe('最迟出发时间(0-24),默认为24。'), sortFlag: z .string() .optional() .default('') .describe( '排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]' ), sortReverse: z .boolean() .optional() .default(false) .describe( '是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。' ), limitedNum: z .number() .min(0) .optional() .default(0) .describe('返回的余票数量限制,默认为0,即不限制。'), format: z .string() .regex(/^(text|csv|json)$/i) .default('text') .optional() .describe( '返回结果格式,默认为text,建议使用text与csv。可选标志:[text, csv, json]' ), }, async ({ date, fromStation, toStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum, format, }) => { // 检查日期是否早于当前日期 if (!checkDate(date)) { return { content: [ { type: 'text', text: 'Error: The date cannot be earlier than today.', }, ], }; } const fromStationResult = parseStationCode(fromStation); const toStationResult = parseStationCode(toStation); if (fromStationResult === null || toStationResult === null) { return { content: [ { type: 'text', text: `Error: Station not found. FromStationResult: ${fromStationResult}, ToStationResult: ${toStationResult}.`, }, ], }; } fromStation = fromStationResult; toStation = toStationResult; const queryParams = new URLSearchParams({ 'leftTicketDTO.train_date': date, 'leftTicketDTO.from_station': fromStation, 'leftTicketDTO.to_station': toStation, purpose_codes: 'ADULT', }); const queryUrl = `${API_BASE}/otn/leftTicket/query`; const cookies = await getCookie(); if (cookies == null || Object.entries(cookies).length === 0) { return { content: [ { type: 'text', text: 'Error: get cookie failed. Check your network.', }, ], }; } const queryResponse = await make12306Request<LeftTicketsQueryResponse>( queryUrl, queryParams, { Cookie: formatCookies(cookies) } ); if (queryResponse === null || queryResponse === undefined) { return { content: [ { type: 'text', text: 'Error: get tickets data failed. ' }, ], }; } const ticketsData = parseTicketsData(queryResponse.data.result); let ticketsInfo: TicketInfo[]; try { ticketsInfo = parseTicketsInfo(ticketsData, queryResponse.data.map); } catch (error) { console.error('Error: parse tickets info failed. ', error); return { content: [ { type: 'text', text: 'Error: parse tickets info failed. ', }, ], }; } const filteredTicketsInfo = filterTicketsInfo<TicketInfo>( ticketsInfo, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum ); var formatedResult; switch (format) { case 'csv': formatedResult = formatTicketsInfoCSV(filteredTicketsInfo); break; case 'json': formatedResult = JSON.stringify(filteredTicketsInfo); break; default: formatedResult = formatTicketsInfo(filteredTicketsInfo); break; } return { content: [ { type: 'text', text: formatedResult, }, ], }; } ); - src/index.ts:1026-1137 (handler)Main handler logic for get-tickets: validates date, parses station codes, queries 12306 API, parses ticket data, filters/sorts results, and formats output.
async ({ date, fromStation, toStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum, format, }) => { // 检查日期是否早于当前日期 if (!checkDate(date)) { return { content: [ { type: 'text', text: 'Error: The date cannot be earlier than today.', }, ], }; } const fromStationResult = parseStationCode(fromStation); const toStationResult = parseStationCode(toStation); if (fromStationResult === null || toStationResult === null) { return { content: [ { type: 'text', text: `Error: Station not found. FromStationResult: ${fromStationResult}, ToStationResult: ${toStationResult}.`, }, ], }; } fromStation = fromStationResult; toStation = toStationResult; const queryParams = new URLSearchParams({ 'leftTicketDTO.train_date': date, 'leftTicketDTO.from_station': fromStation, 'leftTicketDTO.to_station': toStation, purpose_codes: 'ADULT', }); const queryUrl = `${API_BASE}/otn/leftTicket/query`; const cookies = await getCookie(); if (cookies == null || Object.entries(cookies).length === 0) { return { content: [ { type: 'text', text: 'Error: get cookie failed. Check your network.', }, ], }; } const queryResponse = await make12306Request<LeftTicketsQueryResponse>( queryUrl, queryParams, { Cookie: formatCookies(cookies) } ); if (queryResponse === null || queryResponse === undefined) { return { content: [ { type: 'text', text: 'Error: get tickets data failed. ' }, ], }; } const ticketsData = parseTicketsData(queryResponse.data.result); let ticketsInfo: TicketInfo[]; try { ticketsInfo = parseTicketsInfo(ticketsData, queryResponse.data.map); } catch (error) { console.error('Error: parse tickets info failed. ', error); return { content: [ { type: 'text', text: 'Error: parse tickets info failed. ', }, ], }; } const filteredTicketsInfo = filterTicketsInfo<TicketInfo>( ticketsInfo, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum ); var formatedResult; switch (format) { case 'csv': formatedResult = formatTicketsInfoCSV(filteredTicketsInfo); break; case 'json': formatedResult = JSON.stringify(filteredTicketsInfo); break; default: formatedResult = formatTicketsInfo(filteredTicketsInfo); break; } return { content: [ { type: 'text', text: formatedResult, }, ], }; } - src/index.ts:957-1025 (schema)Zod input schema for get-tickets defining parameters: date, fromStation, toStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum, format.
{ date: z .string() .length(10) .describe( '查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。' ), fromStation: z .string() .describe( '出发地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到)' ), toStation: z .string() .describe( '到达地的中文名或站点的 `station_code`(可通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到)' ), trainFilterFlags: z .string() .regex(/^[GDZTKOFS]*$/) .max(8) .optional() .default('') .describe( '车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 "G"。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]' ), earliestStartTime: z .number() .min(0) .max(24) .optional() .default(0) .describe('最早出发时间(0-24),默认为0。'), latestStartTime: z .number() .min(0) .max(24) .optional() .default(24) .describe('最迟出发时间(0-24),默认为24。'), sortFlag: z .string() .optional() .default('') .describe( '排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]' ), sortReverse: z .boolean() .optional() .default(false) .describe( '是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。' ), limitedNum: z .number() .min(0) .optional() .default(0) .describe('返回的余票数量限制,默认为0,即不限制。'), format: z .string() .regex(/^(text|csv|json)$/i) .default('text') .optional() .describe( '返回结果格式,默认为text,建议使用text与csv。可选标志:[text, csv, json]' ), }, - src/index.ts:350-361 (helper)parseTicketsData: parses raw pipe-delimited strings from 12306 into TicketData objects using TicketDataKeys.
function parseTicketsData(rawData: string[]): TicketData[] { const result: TicketData[] = []; for (const item of rawData) { const values = item.split('|'); const entry: Partial<TicketData> = {}; TicketDataKeys.forEach((key, index) => { entry[key] = values[index]; }); result.push(entry as TicketData); } return result; } - src/index.ts:363-407 (helper)parseTicketsInfo: converts TicketData into TicketInfo objects with parsed prices, dates, and station names from the map.
function parseTicketsInfo( ticketsData: TicketData[], map: Record<string, string> ): TicketInfo[] { const result: TicketInfo[] = []; for (const ticket of ticketsData) { const prices = extractPrices( ticket.yp_info_new, ticket.seat_discount_info, ticket ); const dw_flag = extractDWFlags(ticket.dw_flag); const startHours = parseInt(ticket.start_time.split(':')[0]); const startMinutes = parseInt(ticket.start_time.split(':')[1]); const durationHours = parseInt(ticket.lishi.split(':')[0]); const durationMinutes = parseInt(ticket.lishi.split(':')[1]); const startDate = parse( ticket.start_train_date, 'yyyyMMdd', new Date() ); startDate.setHours(startHours, startMinutes); const arriveDate = startDate; arriveDate.setHours( startHours + durationHours, startMinutes + durationMinutes ); result.push({ train_no: ticket.train_no, start_date: format(startDate, 'yyyy-MM-dd'), arrive_date: format(arriveDate, 'yyyy-MM-dd'), start_train_code: ticket.station_train_code, start_time: ticket.start_time, arrive_time: ticket.arrive_time, lishi: ticket.lishi, from_station: map[ticket.from_station_telecode], to_station: map[ticket.to_station_telecode], from_station_telecode: ticket.from_station_telecode, to_station_telecode: ticket.to_station_telecode, prices: prices, dw_flag: dw_flag, }); } return result; }