feat: 初始化MCP时间服务器项目

添加核心功能模块包括时间工具集、服务器实现和测试
配置项目基础设置如ESLint、Prettier和Vitest
实现时间相关功能包括当前时间、时区信息和日期计算
添加README文档说明项目功能和使用方法
This commit is contained in:
yangyudong 2025-08-17 00:39:43 +08:00
parent 8322460c46
commit 82e579e11b
16 changed files with 5290 additions and 1 deletions

269
.gitignore vendored Normal file
View File

@ -0,0 +1,269 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS)
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# VS Code
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# MCP specific
*.mcp
.mcp/
# Test artifacts
test-results/
playwright-report/
test-results.xml

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

307
README.md
View File

@ -1,2 +1,307 @@
# mcp-zero
# MCP Time Server
一个基于 Model Context Protocol (MCP) 的时间工具服务器,提供丰富的时间相关功能。
## 功能特性
### 时间工具
- **getCurrentTime**: 获取当前时间,支持多种时区和格式
- **getTimezone**: 获取时区信息,包括偏移量、缩写等
- **formatTime**: 格式化时间,支持多种格式和语言环境
- **calculateDateDifference**: 计算两个日期之间的差值
- **getWorldClock**: 获取多个时区的世界时钟
### 支持的时区
- 全球主要时区(如 Asia/Shanghai, America/New_York, Europe/London 等)
- UTC 和本地时区
- 自动夏令时检测
### 支持的格式
- ISO 8601 格式
- Unix 时间戳
- 本地化格式
- 自定义格式
## 安装和运行
### 安装依赖
```bash
npm install
```
### 运行服务器
```bash
npm start
```
### 运行测试
```bash
npm test
```
### 代码检查
```bash
npm run lint
```
## 项目结构
```
src/
├── index.js # MCP 服务器入口
├── tools/
│ ├── index.js # 工具导出
│ ├── current-time.js # 当前时间工具
│ ├── timezone.js # 时区信息工具
│ ├── format-time.js # 时间格式化工具
│ ├── date-calculator.js # 日期计算工具
│ └── world-clock.js # 世界时钟工具
tests/
└── time-tools.test.js # 测试文件
```
## 使用示例
### 获取当前时间
```javascript
// 获取 UTC 时间
getCurrentTime({ timezone: 'UTC', format: 'iso' })
// 获取上海时间
getCurrentTime({ timezone: 'Asia/Shanghai', format: 'locale' })
```
### 获取时区信息
```javascript
// 获取时区详细信息
getTimezone({ timezone: 'Asia/Shanghai' })
```
### 格式化时间
```javascript
// 格式化时间
formatTime({
time: '2024-01-01T12:00:00Z',
format: 'custom',
customFormat: 'YYYY-MM-DD HH:mm:ss',
timezone: 'Asia/Shanghai'
})
```
### 计算日期差值
```javascript
// 计算两个日期的差值
calculateDateDifference({
startDate: '2024-01-01',
endDate: '2024-12-31',
unit: 'days'
})
```
### 获取世界时钟
```javascript
// 获取多个时区的时间
getWorldClock({
timezones: ['UTC', 'Asia/Shanghai', 'America/New_York'],
format: 'locale'
})
```
## 技术栈
- **Node.js**: 运行时环境
- **MCP SDK**: Model Context Protocol 支持
- **Zod**: 参数验证
- **Vitest**: 测试框架
- **ESLint**: 代码检查
## 开发
### 添加新工具
1. 在 `src/tools/` 目录下创建新的工具文件
2. 实现工具的 `name``description``inputSchema``handler`
3. 在 `src/tools/index.js` 中导出新工具
4. 添加相应的测试用例
### 代码规范
- 使用 ESLint 进行代码检查
- 遵循 JavaScript 标准代码风格
- 所有工具必须包含完整的参数验证
- 返回格式必须符合 MCP 协议规范
## 许可证
MIT License
## 工具详情
### getCurrentTime
获取当前时间,支持指定时区和格式。
**参数:**
- `timezone` (可选): 时区标识符,如 'UTC', 'America/New_York'
- `format` (可选): 输出格式 ('iso', 'timestamp', 'locale')
**示例:**
```json
{
"timezone": "Asia/Shanghai",
"format": "iso"
}
```
### getTimezone
获取指定时区的详细信息。
**参数:**
- `timezone` (必需): 时区标识符
**示例:**
```json
{
"timezone": "Europe/London"
}
```
### formatTime
将时间格式化为指定格式。
**参数:**
- `time` (必需): 要格式化的时间
- `format` (可选): 输出格式 ('iso', 'timestamp', 'locale', 'custom')
- `timezone` (可选): 目标时区
- `locale` (可选): 本地化设置
- `customFormat` (可选): 自定义格式字符串 (当format为custom时使用)
**示例:**
```json
{
"time": "2024-01-01T12:00:00Z",
"format": "locale",
"timezone": "Asia/Tokyo",
"locale": "ja-JP"
}
```
### calculateDateDifference
计算两个日期之间的差值。
**参数:**
- `startDate` (必需): 开始日期
- `endDate` (必需): 结束日期
- `unit` (可选): 计算单位 ('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds')
- `includeTime` (可选): 是否包含时间部分
**示例:**
```json
{
"startDate": "2024-01-01",
"endDate": "2024-12-31",
"unit": "days"
}
```
### getWorldClock
获取多个时区的当前时间。
**参数:**
- `timezones` (可选): 时区列表,默认为 ['UTC', 'Asia/Shanghai', 'America/New_York', 'Europe/London']
- `format` (可选): 时间格式 ('iso', 'locale', 'custom'),默认为 'locale'
- `customFormat` (可选): 自定义格式模板 (当format为custom时使用)
- `includeOffset` (可选): 是否包含UTC偏移量信息默认为 true
**示例:**
```json
{
"timezones": ["UTC", "America/New_York", "Asia/Shanghai", "Europe/London"],
"format": "locale",
"includeOffset": true
}
```
## 开发
### 项目结构
```
mcp-time-server/
├── src/
│ ├── index.js # 主服务器文件
│ ├── tools/ # 工具模块
│ │ ├── index.js # 工具导出
│ │ ├── current-time.js
│ │ ├── timezone.js
│ │ ├── format-time.js
│ │ ├── date-calculator.js
│ │ └── world-clock.js
│ └── utils/ # 工具函数 (预留)
├── tests/ # 测试文件
└── bin/ # 可执行文件
```
### 运行测试
```bash
npm test
```
### 代码检查
```bash
npm run lint
```
### 代码格式化
```bash
npm run format
```
## 技术栈
- **Node.js** - 运行时环境
- **@modelcontextprotocol/sdk** - MCP 协议支持
- **Zod** - 数据验证
- **Vitest** - 测试框架
- **ESLint** - 代码检查
- **Prettier** - 代码格式化
## 许可证
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request
### 开发指南
1. Fork 项目
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 打开 Pull Request
### 代码规范
- 使用 ESLint 和 Prettier 保持代码风格一致
- 编写测试用例覆盖新功能
- 遵循现有的代码结构和命名约定
- 添加适当的错误处理和参数验证
## 支持
如果您遇到问题或有建议,请:
1. 查看现有的 [Issues](../../issues)
2. 创建新的 Issue 描述问题
3. 提供详细的错误信息和复现步骤
---
**MCP Time Server** - 让时间处理变得简单而强大! 🚀

19
bin/mcp-time.js Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
/**
* MCP Time Server 命令行启动器
*/
import { TimeServer } from '../src/index.js';
async function main() {
try {
const server = new TimeServer();
await server.start();
} catch (error) {
console.error('Failed to start MCP Time Server:', error);
process.exit(1);
}
}
main();

39
eslint.config.js Normal file
View File

@ -0,0 +1,39 @@
import js from "@eslint/js";
import globals from "globals";
export default [
{
files: ["**/*.{js,mjs,cjs}"],
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
...globals.node,
...globals.es2022,
},
},
rules: {
...js.configs.recommended.rules,
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "off",
"prefer-const": "error",
"no-var": "error",
},
},
{
files: ["tests/**/*.js"],
languageOptions: {
globals: {
...globals.node,
...globals.es2022,
describe: "readonly",
it: "readonly",
test: "readonly",
expect: "readonly",
beforeEach: "readonly",
afterEach: "readonly",
vi: "readonly",
},
},
},
];

3337
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "mcp-time-server",
"version": "1.0.0",
"description": "A Model Context Protocol (MCP) server for time-related tools and utilities",
"type": "module",
"main": "src/index.js",
"bin": {
"mcp-time": "./bin/mcp-time.js"
},
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "vitest",
"test:watch": "vitest --watch",
"lint": "eslint src tests",
"lint:fix": "eslint src tests --fix",
"format": "prettier --write src tests",
"format:check": "prettier --check src tests"
},
"repository": {
"type": "git",
"url": "http://60.205.233.184:3010/deastern/mcp-zero.git"
},
"keywords": [
"mcp",
"model-context-protocol",
"time",
"datetime",
"timezone",
"claude",
"ai-tools"
],
"author": "MCP Time Team",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/node": "^24.3.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"vitest": "^3.2.4"
}
}

93
src/index.js Normal file
View File

@ -0,0 +1,93 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { timeTools } from './tools/index.js';
/**
* MCP Time Server
* 提供时间相关工具的Model Context Protocol服务器
*/
class TimeServer {
constructor() {
this.server = new Server(
{
name: 'mcp-time-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
/**
* 设置请求处理器
*/
setupHandlers() {
// 列出可用工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: timeTools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// 调用工具
this.server.setRequestHandler(CallToolRequestSchema, async request => {
const { name, arguments: args } = request.params;
const tool = timeTools.find(t => t.name === name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await tool.handler(args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
throw new Error(`Tool execution failed: ${error.message}`);
}
});
}
/**
* 启动服务器
*/
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
// 优雅关闭处理
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
}
// 启动服务器
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new TimeServer();
server.start().catch(console.error);
}
export { TimeServer };

109
src/tools/current-time.js Normal file
View File

@ -0,0 +1,109 @@
import { z } from 'zod';
/**
* 获取当前时间工具
*/
export const getCurrentTime = {
name: 'getCurrentTime',
description: '获取当前时间,支持指定时区和格式',
inputSchema: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '时区标识符 (例如: Asia/Shanghai, America/New_York)',
default: 'UTC',
},
format: {
type: 'string',
description: '时间格式 (iso, timestamp, locale)',
enum: ['iso', 'timestamp', 'locale'],
default: 'iso',
},
},
},
async handler(args) {
// 参数验证
const schema = z.object({
timezone: z.string().default('UTC'),
format: z.enum(['iso', 'timestamp', 'locale']).default('iso'),
});
const { timezone, format } = schema.parse(args);
try {
const now = new Date();
// 验证时区
let formattedTime;
try {
if (format === 'iso') {
formattedTime =
now
.toLocaleString('sv-SE', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(' ', 'T') + 'Z';
} else if (format === 'timestamp') {
formattedTime = Math.floor(now.getTime() / 1000);
} else {
formattedTime = now.toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
}
} catch {
throw new Error(`无效的时区: ${timezone}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
currentTime: formattedTime,
timezone,
format,
utcTime: now.toISOString(),
timestamp: Math.floor(now.getTime() / 1000),
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
},
};

View File

@ -0,0 +1,250 @@
import { z } from 'zod';
/**
* 日期计算工具
*/
export const calculateDateDifference = {
name: 'calculateDateDifference',
description: '计算两个日期之间的差值,支持多种单位',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'string',
description: '开始日期 (ISO格式或其他可解析格式)',
},
endDate: {
type: 'string',
description: '结束日期 (ISO格式或其他可解析格式)',
},
unit: {
type: 'string',
description: '计算单位',
enum: [
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
],
default: 'days',
},
includeTime: {
type: 'boolean',
description: '是否包含时间部分',
default: true,
},
},
required: ['startDate', 'endDate'],
},
async handler(args) {
// 参数验证
const schema = z.object({
startDate: z.string(),
endDate: z.string(),
unit: z
.enum([
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
])
.default('days'),
includeTime: z.boolean().default(true),
});
const { startDate, endDate, unit, includeTime } = schema.parse(args);
try {
// 解析日期
const start = this.parseDate(startDate);
const end = this.parseDate(endDate);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
throw new Error('无效的日期格式');
}
// 如果不包含时间,则重置为当天开始
if (!includeTime) {
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
}
const difference = this.calculateDifference(start, end, unit);
const absoluteDifference = Math.abs(difference);
// 计算详细信息
const detailedDiff = this.getDetailedDifference(start, end);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
startDate: start.toISOString(),
endDate: end.toISOString(),
difference,
absoluteDifference,
unit,
isEndDateAfter: end > start,
detailed: detailedDiff,
humanReadable: this.formatHumanReadable(detailedDiff),
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
},
/**
* 解析日期字符串
*/
parseDate(dateString) {
// 尝试解析时间戳
if (/^\d+$/.test(dateString)) {
const timestamp = parseInt(dateString);
return new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp);
}
return new Date(dateString);
},
/**
* 计算指定单位的差值
*/
calculateDifference(start, end, unit) {
const diffMs = end.getTime() - start.getTime();
switch (unit) {
case 'milliseconds':
return diffMs;
case 'seconds':
return Math.floor(diffMs / 1000);
case 'minutes':
return Math.floor(diffMs / (1000 * 60));
case 'hours':
return Math.floor(diffMs / (1000 * 60 * 60));
case 'days':
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
case 'months':
return this.calculateMonthDifference(start, end);
case 'years':
return this.calculateYearDifference(start, end);
default:
throw new Error(`不支持的单位: ${unit}`);
}
},
/**
* 计算月份差值
*/
calculateMonthDifference(start, end) {
const yearDiff = end.getFullYear() - start.getFullYear();
const monthDiff = end.getMonth() - start.getMonth();
const dayDiff = end.getDate() - start.getDate();
let totalMonths = yearDiff * 12 + monthDiff;
// 如果结束日期的日期小于开始日期的日期,减去一个月
if (dayDiff < 0) {
totalMonths--;
}
return totalMonths;
},
/**
* 计算年份差值
*/
calculateYearDifference(start, end) {
let years = end.getFullYear() - start.getFullYear();
// 检查是否还没到生日
if (
end.getMonth() < start.getMonth() ||
(end.getMonth() === start.getMonth() && end.getDate() < start.getDate())
) {
years--;
}
return years;
},
/**
* 获取详细的差值信息
*/
getDetailedDifference(start, end) {
const diffMs = Math.abs(end.getTime() - start.getTime());
const years = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 365.25));
const months = Math.floor(
(diffMs % (1000 * 60 * 60 * 24 * 365.25)) / (1000 * 60 * 60 * 24 * 30.44)
);
const days = Math.floor(
(diffMs % (1000 * 60 * 60 * 24 * 30.44)) / (1000 * 60 * 60 * 24)
);
const hours = Math.floor(
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
const milliseconds = diffMs % 1000;
return {
years,
months,
days,
hours,
minutes,
seconds,
milliseconds,
totalDays: Math.floor(diffMs / (1000 * 60 * 60 * 24)),
totalHours: Math.floor(diffMs / (1000 * 60 * 60)),
totalMinutes: Math.floor(diffMs / (1000 * 60)),
totalSeconds: Math.floor(diffMs / 1000),
totalMilliseconds: diffMs,
};
},
/**
* 格式化为人类可读的字符串
*/
formatHumanReadable(detailed) {
const parts = [];
if (detailed.years > 0) parts.push(`${detailed.years}`);
if (detailed.months > 0) parts.push(`${detailed.months}个月`);
if (detailed.days > 0) parts.push(`${detailed.days}`);
if (detailed.hours > 0) parts.push(`${detailed.hours}小时`);
if (detailed.minutes > 0) parts.push(`${detailed.minutes}分钟`);
if (detailed.seconds > 0) parts.push(`${detailed.seconds}`);
return parts.length > 0 ? parts.join(' ') : '0秒';
},
};

189
src/tools/format-time.js Normal file
View File

@ -0,0 +1,189 @@
import { z } from 'zod';
/**
* 时间格式化工具
*/
export const formatTime = {
name: 'formatTime',
description: '格式化时间为指定格式',
inputSchema: {
type: 'object',
properties: {
time: {
type: 'string',
description: '要格式化的时间 (ISO格式、时间戳或其他可解析格式)',
},
format: {
type: 'string',
description: '输出格式 (iso, timestamp, locale, custom)',
enum: ['iso', 'timestamp', 'locale', 'custom'],
default: 'iso',
},
timezone: {
type: 'string',
description: '目标时区',
default: 'UTC',
},
customFormat: {
type: 'string',
description: '自定义格式模板 (当format为custom时使用)',
},
locale: {
type: 'string',
description: '本地化语言代码',
default: 'zh-CN',
},
},
required: ['time'],
},
async handler(args) {
// 参数验证
const schema = z.object({
time: z.string(),
format: z.enum(['iso', 'timestamp', 'locale', 'custom']).default('iso'),
timezone: z.string().default('UTC'),
customFormat: z.string().optional(),
locale: z.string().default('zh-CN'),
});
const { time, format, timezone, customFormat, locale } = schema.parse(args);
try {
// 解析输入时间
let date;
if (/^\d+$/.test(time)) {
// 时间戳(秒或毫秒)
const timestamp = parseInt(time);
date = new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp);
} else {
date = new Date(time);
}
if (isNaN(date.getTime())) {
throw new Error('无效的时间格式');
}
let formattedTime;
try {
switch (format) {
case 'iso':
formattedTime =
date
.toLocaleString('sv-SE', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(' ', 'T') + 'Z';
break;
case 'timestamp':
formattedTime = Math.floor(date.getTime() / 1000);
break;
case 'locale':
formattedTime = date.toLocaleString(locale, {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
break;
case 'custom':
if (!customFormat) {
throw new Error('自定义格式需要提供customFormat参数');
}
formattedTime = this.formatCustom(date, customFormat, timezone);
break;
default:
throw new Error(`不支持的格式: ${format}`);
}
} catch (error) {
if (error.message.includes('Invalid time zone')) {
throw new Error(`无效的时区: ${timezone}`);
}
throw error;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
originalTime: time,
formattedTime,
format,
timezone,
locale,
utcTime: date.toISOString(),
timestamp: Math.floor(date.getTime() / 1000),
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
},
/**
* 自定义格式化
*/
formatCustom(date, format, timezone) {
const options = {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const parts = new Intl.DateTimeFormat('en-CA', options).formatToParts(date);
const partMap = {};
parts.forEach(part => {
partMap[part.type] = part.value;
});
return format
.replace(/YYYY/g, partMap.year)
.replace(/MM/g, partMap.month)
.replace(/DD/g, partMap.day)
.replace(/HH/g, partMap.hour)
.replace(/mm/g, partMap.minute)
.replace(/ss/g, partMap.second);
},
};

24
src/tools/index.js Normal file
View File

@ -0,0 +1,24 @@
import { getCurrentTime } from './current-time.js';
import { getTimezone } from './timezone.js';
import { formatTime } from './format-time.js';
import { calculateDateDifference } from './date-calculator.js';
import { getWorldClock } from './world-clock.js';
/**
* 所有可用的时间工具
*/
export const timeTools = [
getCurrentTime,
getTimezone,
formatTime,
calculateDateDifference,
getWorldClock,
];
export {
getCurrentTime,
getTimezone,
formatTime,
calculateDateDifference,
getWorldClock,
};

163
src/tools/timezone.js Normal file
View File

@ -0,0 +1,163 @@
import { z } from 'zod';
/**
* 时区信息工具
*/
export const getTimezone = {
name: 'getTimezone',
description: '获取时区信息,包括偏移量、缩写等',
inputSchema: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '时区标识符 (例如: Asia/Shanghai, America/New_York)',
default: 'UTC',
},
date: {
type: 'string',
description: '指定日期 (ISO格式), 默认为当前时间',
},
},
},
async handler(args) {
// 参数验证
const schema = z.object({
timezone: z.string().default('UTC'),
date: z.string().optional(),
});
const { timezone, date } = schema.parse(args);
try {
const targetDate = date ? new Date(date) : new Date();
if (isNaN(targetDate.getTime())) {
throw new Error('无效的日期格式');
}
// 验证时区并获取信息
let timezoneInfo;
try {
// 获取时区偏移量
const utcTime = targetDate.getTime();
const localTime = new Date(
targetDate.toLocaleString('en-US', { timeZone: timezone })
).getTime();
const offset = (localTime - utcTime) / (1000 * 60 * 60);
// 获取时区缩写
const formatter = new Intl.DateTimeFormat('en', {
timeZone: timezone,
timeZoneName: 'short',
});
const parts = formatter.formatToParts(targetDate);
const abbreviation =
parts.find(part => part.type === 'timeZoneName')?.value || '';
// 获取长格式名称
const longFormatter = new Intl.DateTimeFormat('en', {
timeZone: timezone,
timeZoneName: 'long',
});
const longParts = longFormatter.formatToParts(targetDate);
const longName =
longParts.find(part => part.type === 'timeZoneName')?.value || '';
timezoneInfo = {
timezone,
offset,
offsetString: `UTC${offset >= 0 ? '+' : ''}${offset}`,
abbreviation,
longName,
isDST: this.isDaylightSavingTime(targetDate, timezone),
};
} catch {
throw new Error(`无效的时区: ${timezone}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
...timezoneInfo,
date: targetDate.toISOString(),
localTime: targetDate.toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}),
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
},
/**
* 检查是否为夏令时
*/
isDaylightSavingTime(date, timezone) {
try {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = this.getTimezoneOffset(jan, timezone);
const julOffset = this.getTimezoneOffset(jul, timezone);
const currentOffset = this.getTimezoneOffset(date, timezone);
return currentOffset !== Math.max(janOffset, julOffset);
} catch {
return false;
}
},
/**
* 获取时区偏移量分钟
*/
getTimezoneOffset(date, timezone) {
const utc = date.getTime() + date.getTimezoneOffset() * 60000;
const local = new Date(
utc + this.getTimezoneOffsetHours(date, timezone) * 3600000
);
return (local.getTime() - utc) / 60000;
},
/**
* 获取时区偏移量小时
*/
getTimezoneOffsetHours(date, timezone) {
const utcTime = date.getTime();
const localTime = new Date(
date.toLocaleString('en-US', { timeZone: timezone })
).getTime();
return (localTime - utcTime) / (1000 * 60 * 60);
},
};

277
src/tools/world-clock.js Normal file
View File

@ -0,0 +1,277 @@
import { z } from 'zod';
/**
* 世界时钟工具
*/
export const getWorldClock = {
name: 'getWorldClock',
description: '获取多个时区的当前时间',
inputSchema: {
type: 'object',
properties: {
timezones: {
type: 'array',
items: {
type: 'string',
},
description:
'时区列表 (例如: ["Asia/Shanghai", "America/New_York", "Europe/London"])',
default: ['UTC', 'Asia/Shanghai', 'America/New_York', 'Europe/London'],
},
format: {
type: 'string',
description: '时间格式',
enum: ['iso', 'locale', 'custom'],
default: 'locale',
},
customFormat: {
type: 'string',
description: '自定义格式模板 (当format为custom时使用)',
},
includeOffset: {
type: 'boolean',
description: '是否包含UTC偏移量信息',
default: true,
},
},
},
async handler(args) {
// 参数验证
const schema = z.object({
timezones: z
.array(z.string())
.default(['UTC', 'Asia/Shanghai', 'America/New_York', 'Europe/London']),
format: z.enum(['iso', 'locale', 'custom']).default('locale'),
customFormat: z.string().optional(),
includeOffset: z.boolean().default(true),
});
const { timezones, format, customFormat, includeOffset } =
schema.parse(args);
try {
const now = new Date();
const worldTimes = [];
const errors = [];
for (const timezone of timezones) {
try {
const timeInfo = await this.getTimezoneTime(
now,
timezone,
format,
customFormat,
includeOffset
);
worldTimes.push(timeInfo);
} catch (error) {
errors.push({
timezone,
error: error.message,
});
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
timestamp: Math.floor(now.getTime() / 1000),
utcTime: now.toISOString(),
worldClock: worldTimes,
errors: errors.length > 0 ? errors : undefined,
totalTimezones: timezones.length,
successfulTimezones: worldTimes.length,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
},
/**
* 获取指定时区的时间信息
*/
async getTimezoneTime(date, timezone, format, customFormat, includeOffset) {
try {
let formattedTime;
switch (format) {
case 'iso':
formattedTime =
date
.toLocaleString('sv-SE', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(' ', 'T') + 'Z';
break;
case 'locale':
formattedTime = date.toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
weekday: 'long',
});
break;
case 'custom':
if (!customFormat) {
throw new Error('自定义格式需要提供customFormat参数');
}
formattedTime = this.formatCustom(date, customFormat, timezone);
break;
default:
throw new Error(`不支持的格式: ${format}`);
}
const result = {
timezone,
time: formattedTime,
format,
};
if (includeOffset) {
const offsetInfo = this.getTimezoneOffset(date, timezone);
result.offset = offsetInfo;
}
return result;
} catch (error) {
throw new Error(`时区 ${timezone} 处理失败: ${error.message}`);
}
},
/**
* 获取时区偏移信息
*/
getTimezoneOffset(date, timezone) {
try {
// 获取UTC时间和本地时间的差值
const utcTime = date.getTime();
const localTime = new Date(
date.toLocaleString('en-US', { timeZone: timezone })
).getTime();
const offsetMs = localTime - utcTime;
const offsetHours = offsetMs / (1000 * 60 * 60);
// 获取时区缩写
const formatter = new Intl.DateTimeFormat('en', {
timeZone: timezone,
timeZoneName: 'short',
});
const parts = formatter.formatToParts(date);
const abbreviation =
parts.find(part => part.type === 'timeZoneName')?.value || '';
return {
hours: offsetHours,
string: `UTC${offsetHours >= 0 ? '+' : ''}${offsetHours}`,
abbreviation,
isDST: this.isDaylightSavingTime(date, timezone),
};
} catch (error) {
return {
hours: 0,
string: 'UTC+0',
abbreviation: '',
isDST: false,
error: error.message,
};
}
},
/**
* 检查是否为夏令时
*/
isDaylightSavingTime(date, timezone) {
try {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = this.getTimezoneOffsetHours(jan, timezone);
const julOffset = this.getTimezoneOffsetHours(jul, timezone);
const currentOffset = this.getTimezoneOffsetHours(date, timezone);
return currentOffset !== Math.max(janOffset, julOffset);
} catch {
return false;
}
},
/**
* 获取时区偏移量小时
*/
getTimezoneOffsetHours(date, timezone) {
const utcTime = date.getTime();
const localTime = new Date(
date.toLocaleString('en-US', { timeZone: timezone })
).getTime();
return (localTime - utcTime) / (1000 * 60 * 60);
},
/**
* 自定义格式化
*/
formatCustom(date, format, timezone) {
const options = {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const parts = new Intl.DateTimeFormat('en-CA', options).formatToParts(date);
const partMap = {};
parts.forEach(part => {
partMap[part.type] = part.value;
});
return format
.replace(/YYYY/g, partMap.year)
.replace(/MM/g, partMap.month)
.replace(/DD/g, partMap.day)
.replace(/HH/g, partMap.hour)
.replace(/mm/g, partMap.minute)
.replace(/ss/g, partMap.second);
},
};

135
tests/time-tools.test.js Normal file
View File

@ -0,0 +1,135 @@
import { describe, it, expect } from 'vitest';
import { timeTools } from '../src/tools/index.js';
describe('Time Tools', () => {
describe('getCurrentTime', () => {
it('should return current time in ISO format by default', async () => {
const getCurrentTime = timeTools.find(
tool => tool.name === 'getCurrentTime'
);
expect(getCurrentTime).toBeDefined();
const result = await getCurrentTime.handler({});
expect(result).toHaveProperty('content');
expect(result.content[0]).toHaveProperty('text');
const response = JSON.parse(result.content[0].text);
expect(response).toHaveProperty('currentTime');
expect(response).toHaveProperty('timezone');
expect(response).toHaveProperty('format');
});
it('should return time in specified timezone', async () => {
const getCurrentTime = timeTools.find(
tool => tool.name === 'getCurrentTime'
);
const result = await getCurrentTime.handler({
timezone: 'America/New_York',
});
const response = JSON.parse(result.content[0].text);
expect(response.timezone).toBe('America/New_York');
});
});
describe('getTimezone', () => {
it('should return timezone information', async () => {
const getTimezone = timeTools.find(tool => tool.name === 'getTimezone');
expect(getTimezone).toBeDefined();
const result = await getTimezone.handler({ timezone: 'UTC' });
const response = JSON.parse(result.content[0].text);
expect(response).toHaveProperty('timezone');
expect(response).toHaveProperty('offset');
expect(response).toHaveProperty('abbreviation');
expect(response).toHaveProperty('longName');
});
});
describe('formatTime', () => {
it('should format time correctly', async () => {
const formatTime = timeTools.find(tool => tool.name === 'formatTime');
expect(formatTime).toBeDefined();
const testDate = '2024-01-01T12:00:00Z';
const result = await formatTime.handler({
time: testDate,
format: 'iso',
});
const response = JSON.parse(result.content[0].text);
expect(response).toHaveProperty('originalTime');
expect(response).toHaveProperty('formattedTime');
expect(response).toHaveProperty('format');
});
});
describe('calculateDateDifference', () => {
it('should calculate date difference correctly', async () => {
const calculateDateDifference = timeTools.find(
tool => tool.name === 'calculateDateDifference'
);
expect(calculateDateDifference).toBeDefined();
const result = await calculateDateDifference.handler({
startDate: '2024-01-01',
endDate: '2024-01-02',
unit: 'days',
});
const response = JSON.parse(result.content[0].text);
expect(response).toHaveProperty('startDate');
expect(response).toHaveProperty('endDate');
expect(response).toHaveProperty('difference');
expect(response.difference).toBe(1);
});
});
describe('getWorldClock', () => {
it('should return world clock information', async () => {
const getWorldClock = timeTools.find(
tool => tool.name === 'getWorldClock'
);
expect(getWorldClock).toBeDefined();
const result = await getWorldClock.handler({
timezones: ['UTC', 'America/New_York'],
});
const response = JSON.parse(result.content[0].text);
expect(response).toHaveProperty('worldClock');
expect(Array.isArray(response.worldClock)).toBe(true);
expect(response.worldClock).toHaveLength(2);
});
});
});
describe('Tool Validation', () => {
it('should have all required tools', () => {
const expectedTools = [
'getCurrentTime',
'getTimezone',
'formatTime',
'calculateDateDifference',
'getWorldClock',
];
expectedTools.forEach(toolName => {
const tool = timeTools.find(t => t.name === toolName);
expect(tool).toBeDefined();
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
expect(tool).toHaveProperty('handler');
});
});
it('should have valid input schemas', () => {
timeTools.forEach(tool => {
expect(tool.inputSchema).toHaveProperty('type');
expect(tool.inputSchema.type).toBe('object');
expect(tool.inputSchema).toHaveProperty('properties');
});
});
});

18
vitest.config.js Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'bin/',
'*.config.js',
],
},
},
});