get-tickets
Check available train tickets on China's 12306 system by specifying travel date, stations, and optional filters like train types or departure times.
Instructions
查询12306余票信息。
Input Schema
TableJSON 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:1011-1116 (handler)Core handler logic: validates date and stations, fetches ticket data from 12306 API, parses raw data into structured TicketInfo using helpers, applies user-specified filters/sorting/limit, formats output in text/CSV/JSON, returns MCP content.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.', }, ], }; } if ( !Object.keys(STATIONS).includes(fromStation) || !Object.keys(STATIONS).includes(toStation) ) { return { content: [{ type: 'text', text: 'Error: Station not found. ' }], }; } 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:942-1010 (schema)Zod input schema defining parameters for the get-tickets tool, including date, station codes, filters, sorting options, limits, and output 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:939-1117 (registration)MCP server tool registration for 'get-tickets', specifying name, Chinese description, input schema, and inline handler function.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.', }, ], }; } if ( !Object.keys(STATIONS).includes(fromStation) || !Object.keys(STATIONS).includes(toStation) ) { return { content: [{ type: 'text', text: 'Error: Station not found. ' }], }; } 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/types.ts:122-136 (schema)TypeScript type definitions for TicketInfo, TicketData, Price, etc., used in parsing API responses and structuring tool output data.export type TicketInfo = { train_no: string; start_train_code: string; start_date: string; start_time: string; arrive_date: string; arrive_time: string; lishi: string; from_station: string; to_station: string; from_station_telecode: string; to_station_telecode: string; prices: Price[]; dw_flag: string[]; };
- src/index.ts:480-531 (helper)Key helper: filters tickets by train type (G/D/etc.), time range, sorts by time/duration, limits results. Called directly in handler.function filterTicketsInfo<T extends TicketInfo | InterlineInfo>( ticketsInfo: T[], trainFilterFlags: string, earliestStartTime: number = 0, latestStartTime: number = 24, sortFlag: string = '', sortReverse: boolean = false, limitedNum: number = 0 ): T[] { let result: T[]; // FilterFlags过滤 if (trainFilterFlags.length === 0) { result = ticketsInfo; } else { result = []; for (const ticketInfo of ticketsInfo) { for (const filter of trainFilterFlags) { if ( TRAIN_FILTERS[filter as keyof typeof TRAIN_FILTERS]( ticketInfo ) ) { result.push(ticketInfo); break; } } } } // startTime 过滤 result = result.filter((ticketInfo) => { const startTimeHour = parseInt(ticketInfo.start_time.split(':')[0], 10); if ( startTimeHour >= earliestStartTime && startTimeHour < latestStartTime ) { return true; } return false; }); // sort排序 if (Object.keys(TIME_COMPARETOR).includes(sortFlag)) { result.sort(TIME_COMPARETOR[sortFlag as keyof typeof TIME_COMPARETOR]); if (sortReverse) { result.reverse(); } } if (limitedNum == 0) { return result; } return result.slice(0, limitedNum); }