feat: 初始化MCP时间服务器项目
添加核心功能模块包括时间工具集、服务器实现和测试 配置项目基础设置如ESLint、Prettier和Vitest 实现时间相关功能包括当前时间、时区信息和日期计算 添加README文档说明项目功能和使用方法
This commit is contained in:
parent
8322460c46
commit
82e579e11b
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',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user