Skip to main content
Glama
timeaissr

datetime-mcp-server

by timeaissr
README.md
# datetime MCP Server

一个提供当前日期/时间查询的 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,采用 TypeScript 编写,设计为部署在 AWS Lambda 上,通过 Streamable HTTP 对外提供服务。

## 功能特性

- **两个 MCP 工具**:`get_date`、`get_datetime`,输出 ISO 8601 格式
- **可选时区参数**:支持 IANA 时区名(`Asia/Shanghai`)和 UTC 偏移量(`+08:00`),默认 UTC
- **零第三方时间库**:完全基于 Node.js 内建 `Intl`(ICU),无 moment/luxon/dayjs 依赖
- **Serverless 部署**:单文件 bundle(约 2MB)+ Lambda Web Adapter,冷启动后热实例毫秒级响应
- **符合最新 MCP 标准**:Streamable HTTP 传输、stateless 运行模式、协议版本协商由官方 SDK 处理

## 架构

```
MCP Client ──HTTP POST /mcp──▶ Function URL ──▶ Lambda 函数
                                                │
                                                ├─ LWA Layer (AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap)
                                                │    └─ 执行 zip 根目录 bootstrap → node dist/index.js
                                                │
                                                └─ Express app (端口 8080)
                                                     └─ POST /mcp → StreamableHTTPServerTransport
                                                          (stateless:每请求新建 transport,
                                                            enableJsonResponse 返回普通 JSON)
```

关键机制:

| 组件 | 说明 |
|---|---|
| Streamable HTTP | MCP 当前唯一推荐的 HTTP 传输;HTTP+SSE 已废弃 |
| Stateless 模式 | 每个请求自包含、不维护会话,与 Lambda 的冻结/解冻模型契合 |
| `enableJsonResponse: true` | 响应走普通 JSON 而非 SSE 流,避开 Lambda 响应缓冲限制 |
| Lambda Web Adapter (LWA) | AWS 官方公共 Layer,把 Lambda invoke 翻译成对 localhost:8080 的真实 HTTP 请求 |
| `bootstrap` 脚本 | LWA 接管后的真正进程入口(`node dist/index.js`);配置里的 handler 字段只是占位符 |

## MCP 工具

### `get_date`

返回当前日历日期,ISO 8601 `YYYY-MM-DD`。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `timezone` | string | 否 | IANA 时区名或 UTC 偏移量,默认 `"UTC"` |

```json
{"result":{"content":[{"type":"text","text":"2026-08-24"}]}}
```

### `get_datetime`

返回当前日期时间,ISO 8601 带偏移量后缀(UTC 输出用 `Z`)。

```json
{"result":{"content":[{"type":"text","text":"2026-08-23T14:30:00+08:00"}]}}
```

### 错误行为

- 时区缺失或空白字符串 → 视为 UTC
- 非空但非法的时区 → 返回 `isError: true` 结果,附提示信息:

```
Error: Invalid IANA timezone "Mars/Olympus". Use a name like "UTC", "Asia/Shanghai" or "America/New_York".
```

- 协议层错误(如缺少 Accept 头)由 SDK 返回标准 JSON-RPC error

## 时间来源与精度

时间读取自运行环境的系统时钟(`new Date()`)。本地即本机时钟(依赖 NTP 校准);在 Lambda 上宿主机时钟由 **Amazon Time Sync Service**(GPS/原子钟源,PTP/NTP)自动同步,误差毫秒级以内,无需任何配置。

刻意不做每次查询外部 NTP:AWS 时钟已持续同步,外查只会增加延迟(+10~100ms)、引入网络故障面和限流问题,准确度不会更高。

输出分辨率精确到秒(毫秒被截断)。时区转换是纯算术——所有时区拿到的是同一绝对瞬间,只是墙上时间表示不同。

## 本地开发

要求 Node.js ≥ 22。

```bash
npm install        # 安装依赖
npm run dev        # 构建 + 启动(默认 http://localhost:3000/mcp)
npm test           # vitest 单测(14 个用例)
npm run typecheck  # tsc --noEmit
npm run lint       # eslint
npm run build      # esbuild 打包为 CJS 单文件 dist/index.js
```

本地手动测试(Accept 头必须同时声明两种媒体类型,这是 SDK 的协议校验):

```bash
curl -s http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_datetime","arguments":{"timezone":"Asia/Shanghai"}}}'
```

也可使用官方调试器:`npx @modelcontextprotocol/inspector`,连接类型选 Streamable HTTP。

> 注意本机若设置了 `http_proxy` 环境变量,curl 需加 `--noproxy '*'`,否则 localhost 请求会被代理拦截返回 502。

## 项目结构

```
├── src/
│   ├── time.ts      # 核心纯函数:ISO 格式化、时区校验(可独立单测)
│   ├── server.ts    # McpServer 实例 + 工具注册
│   └── index.ts     # Express app + stateless transport 挂载 + 进程入口
├── test/
│   └── time.test.ts # 固定时间戳验证多时区/DST/半小时偏移/错误输入
├── build.mjs        # esbuild 构建脚本(bundle: platform=node22, format=cjs)
├── bootstrap        # LWA 进入口(zip 根目录,需可执行位 755)
├── deploy.sh        # CLI 一键部署脚本
├── function.zip     # 部署包(构建产物,不入库)
└── dist/index.js    # 打包产物(约 2MB,零运行时依赖安装)
```

## 技术实现要点

时区格式化全部通过 `Intl.DateTimeFormat` 完成,核心技巧:

1. **日期**:`en-CA` locale 天然输出 `YYYY-MM-DD`,无需手工拼装
2. **带偏移量的时间**:`formatToParts` 取目标时区的墙上时间各字段 → 用 `Date.UTC()` 把它当作 UTC 反算 → 与原始 instant 相减得到分钟级 offset → 格式化为 `±HH:MM` 或 `Z`
3. **`hourCycle: "h23"`**:避免 `hour12: false` 在某些 locale 下产生 `"24"` 小时的边界 bug
4. **DST 与特殊时区**:offset 按请求时刻计算,夏令时切换自然正确(纽约 `-04:00`→`-05:00`),半小时/刻钟时区(印度 `+05:30`、尼泊尔 `+05:45`、查塔姆 `+12:45`)均被单测覆盖
5. **Formatter 缓存**:按 timezone 缓存 `Intl.DateTimeFormat` 实例,热实例上重复调用零构造开销
6. **时区校验**:try/catch 构造 `Intl.DateTimeFormat`,非法值抛 `RangeError` 即视为无效。新版 ECMA-402 使 ICU 同时接受 UTC 偏移量字符串(如 `+08:00`),已验证整条链路输出正确并纳入测试

## 部署到 AWS Lambda

前置条件:AWS CLI 已配置凭证;一个含 `AWSLambdaBasicExecutionRole` 权限的执行角色(没有可用下面命令创建):

```bash
aws iam create-role --role-name datetime-mcp-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'

aws iam attach-role-policy --role-name datetime-mcp-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

### 方式一:CLI 脚本

```bash
ROLE_ARN=arn:aws:iam::<账号ID>:role/datetime-mcp-role ./deploy.sh
```

可选环境变量:`AWS_REGION`(默认 us-east-1)、`FUNCTION_NAME`(默认 datetime-mcp)、`ARCH`(arm64/x86_64,默认 arm64)。

脚本流程:动态查询区域最新的 LWA Layer 版本 → esbuild 构建 → bsdtar 打包(系统无 zip 命令)→ create/update 函数(nodejs22.x、512MB、超时 10s)→ 创建无鉴权 Function URL 并授权公开调用 → 输出 endpoint。重复运行安全,自动走更新路径。

### 方式二:AWS 控制台

1. 本地打包:
   ```bash
   npm run build && bsdtar --format zip -cf function.zip bootstrap dist/index.js
   ```
2. Lambda 控制台 → Create function → Author from scratch
   - Runtime:**Node.js 22.x**;Architecture:**arm64**
   - Permissions:Create a new role with basic Lambda permissions
3. Code 页签 → Upload from → .zip file → 选择 `function.zip`(内部结构必须是根目录 `bootstrap` + `dist/index.js`)
4. Layers 区 → Add a layer → AWS layers → **LambdaAdapterLayerArm64**(选最新版本)
5. Configuration → Environment variables:
   | Key | Value |
   |---|---|
   | `AWS_LAMBDA_EXEC_WRAPPER` | `/opt/bootstrap` |
   | `PORT` | `8080` |
6. Configuration → General configuration → Memory **512 MB**、Timeout **10 s**
7. Configuration → Function URL → Create → Auth type **NONE**

> 控制台创建 NONE 类型 Function URL 时会自动添加公开调用的资源策略,无需再执行 `add-permission`(这是与 CLI 的差异)。

浏览器直接访问 Function URL 返回 405 属正常(仅允许 POST)。以后更新代码:重新构建打包后在 Code 页签重新 Upload 即可。

## MCP 客户端接入

以 opencode 为例,在配置中添加 remote MCP server:

```json
{
  "mcp": {
    "datetime": {
      "type": "remote",
      "url": "https://<identifier>.lambda-url.<region>.on.aws/mcp"
    }
  }
}
```

## 设计决策记录

| 决策 | 结论 | 理由 |
|---|---|---|
| 语言 | TypeScript | `Intl` 内建时区支持零依赖;Lambda 冷启动最快(~100-200ms vs Python ~300-800ms);bundle 小。Python 优势是装饰器写法更简洁 |
| 传输模式 | Streamable HTTP + stateless + JSON response | Lambda 无持久进程,stdio 不可行;无会话状态天然匹配冻结/解冻模型;纯 JSON 避开 SSE 流式限制 |
| SDK 版本 | 稳定版 `@modelcontextprotocol/sdk@1.x`(1.30.0) | 正确实现协议版本协商,向后兼容旧客户端;2026-07-28 修订版的新特性(TTL 缓存等)对静态工具列表无意义;beta 拆分包 API 未稳定 |
| 时区实现 | `Intl` 而非第三方库 | 零依赖、Lambda 上无需补装 tzdata、精度足够 |
| 时间源 | 系统时钟而非每次查 NTP | AWS 已做亚毫秒同步;NTP 外查增加延迟/故障面/成本且无准确度收益 |
| 部署 | 手动 CLI/控制台 + LWA Layer | 不引入 SAM/CDK/Terraform 的前期投入;deploy.sh 仅是命令批处理,非 IaC |

## 故障排查

| 现象 | 原因与解决 |
|---|---|
| `Not Acceptable: Client must accept both application/json and text/event-stream` | 请求缺 Accept 头。curl 测试需带 `-H "Accept: application/json, text/event-stream"`;真实 MCP 客户端自动携带 |
| `Handler 'handler' missing on module 'index'` | 忘记挂 LWA Layer 或漏设 `AWS_LAMBDA_EXEC_WRAPPER`,运行时按默认路径找 handler 导致。补齐 Layer 和环境变量即可 |
| GET /mcp 返回 405 | 符合预期。stateless 模式不支持服务端推送流,仅接受 POST |
| curl 本地测试返回 502 且响应极快 | 系统代理拦截了 localhost。加 `--noproxy '*'` |
| Function URL 冷启动首个请求慢 | Lambda 冻结/解冻机制所致,后续请求毫秒级 |
| 打包时报 `zip: command not found` | 本机未装 zip 包(Arch 上与 unzip 是两个包)。本项目已改用 `bsdtar`(libarchive 自带),保留权限位 |

## 已知限制与未来改进

- **防御性 handler 未添加**:若裸跑于默认 Node.js 运行时(无 LWA),报错信息晦涩。可导出一个提示性的占位 handler 改善误配置时的报错体验,对正常路径零影响
- 输出仅到秒级精度,未暴露毫秒
- 未做鉴权与限流——公开 endpoint 可能被滥用刷调用量(虽然单次成本趋近于零)
- `Intl.supportedValuesOf("timeZone")` 的别名表未用于参数提示,错误信息只给了三个示例
- SDK 未来从稳定版 1.x 迁移到拆分包(支持 2026-07-28 修订版)时需跟进 API 变更

## License

[ISC](./LICENSE)