feat: 初始化MCP时间服务器项目
添加核心功能模块包括时间工具集、服务器实现和测试 配置项目基础设置如ESLint、Prettier和Vitest 实现时间相关功能包括当前时间、时区信息和日期计算 添加README文档说明项目功能和使用方法
This commit is contained in:
269
.gitignore
vendored
Normal file
269
.gitignore
vendored
Normal 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
11
.prettierrc
Normal 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
307
README.md
@@ -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
19
bin/mcp-time.js
Executable 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
39
eslint.config.js
Normal 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
3337
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
package.json
Normal file
51
package.json
Normal 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
93
src/index.js
Normal 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
109
src/tools/current-time.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
250
src/tools/date-calculator.js
Normal file
250
src/tools/date-calculator.js
Normal 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
189
src/tools/format-time.js
Normal 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
24
src/tools/index.js
Normal 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
163
src/tools/timezone.js
Normal 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
277
src/tools/world-clock.js
Normal 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
135
tests/time-tools.test.js
Normal 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
18
vitest.config.js
Normal 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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Reference in New Issue
Block a user