# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── update-tyme4ts.md
├── .github
│ └── workflows
│ ├── release.yml
│ └── update-tyme4ts.yml
├── .gitignore
├── .nvmrc
├── biome.json
├── CHANGELOG.md
├── Dockerfile
├── icon.svg
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.en.md
├── README.md
├── rslib.config.ts
├── smithery.yaml
├── src
│ ├── almanac.ts
│ ├── index.ts
│ ├── server.ts
│ ├── types.ts
│ └── utils.ts
├── tests
│ ├── index.test.ts
│ └── tsconfig.json
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
18
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
# IDE
.vscode/*
!.vscode/extensions.json
.idea
```
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
```markdown
# Tung Shing MCP Server
[](https://smithery.ai/server/@baranwang/mcp-tung-shing)
[](https://www.npmjs.com/package/mcp-tung-shing)
[](https://github.com/baranwang/mcp-tung-shing/blob/main/LICENSE)
[中文文档](./README.md) | English
> Chinese Traditional Almanac calculation service based on Model Context Protocol (MCP)
## ✨ Features
- 📅 **Calendar Conversion** - Convert between Gregorian and Chinese lunar calendar
- 🍀 **Daily Guidance** - Detailed information on recommended and avoided activities for each day
- 🕐 **Time Periods** - Fortune information for the twelve traditional Chinese time periods
- 🔮 **Metaphysical Elements** - Detailed data on five elements, deities, star constellations and other traditional metaphysical information
## 🚀 Installation & Usage
Add the following to your MCP configuration file:
```json
{
"mcpServers": {
"tung-shing": {
"command": "npx",
"args": [
"-y",
"mcp-tung-shing@latest"
]
}
}
}
```
## ⚙️ Tools
### get-tung-shing
Get almanac information for specified date(s)
**Parameters:**
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `startDate` | String | No | Today | Start date, format: "YYYY-MM-DD" |
| `days` | Number | No | 1 | Number of days to retrieve |
| `includeHours` | Boolean | No | false | Whether to include hourly information |
| `tabooFilters` | Array | No | - | Filter for recommended and avoided activities, conditions are in OR relationship |
| `tabooFilters[].type` | 1 \| 2 | Yes | - | Filter type: recommends(1), avoids(2) |
| `tabooFilters[].value` | String | Yes | - | The activity to filter |
## 🤝 Contributing
Issues and Pull Requests are welcome to improve this project.
```
--------------------------------------------------------------------------------
/.changeset/update-tyme4ts.md:
--------------------------------------------------------------------------------
```markdown
---
"mcp-tung-shing": patch
---
bump tyme4ts version to 1.3.4
```
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "../tsconfig.json",
"include": [".", "../vitest.setup.ts"]
}
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
// Configure Vitest (https://vitest.dev/config/)
test: {},
});
```
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
{}
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['dist/index.cjs'],
})
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"lib": ["ES2021"],
"module": "ESNext",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"useDefineForClassFields": true,
"allowImportingTsExtensions": true
},
"include": ["src"]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:18-alpine as builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
FROM node:18-alpine as runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
CMD ["node", "dist/index.cjs"]
```
--------------------------------------------------------------------------------
/rslib.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from '@rslib/core';
import { version } from "./package.json";
export default defineConfig({
lib: [
{
format: 'esm',
syntax: 'es2021',
dts: true,
},
{
format: 'cjs',
syntax: 'es2021',
},
],
source: {
define: {
'process.env.PACKAGE_VERSION': JSON.stringify(version),
}
}
});
```
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
"organizeImports": {
"enabled": true
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"formatter": {
"indentStyle": "space"
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"css": {
"parser": {
"cssModules": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import dayjs from 'dayjs';
import { PluginLunar } from 'dayjs-plugin-lunar';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createServer } from './server';
// 初始化日期插件
dayjs.extend(PluginLunar);
// 启动服务器
(async () => {
try {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Tung Shing MCP server started');
} catch (error) {
console.error('Failed to start Tung Shing MCP server:', error);
process.exit(1);
}
})();
```
--------------------------------------------------------------------------------
/tests/index.test.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { beforeAll, expect, test } from 'vitest';
const transport = new StdioClientTransport({
command: "node",
args: ["."]
});
const client = new Client({
name: 'test-client',
version: '0.0.0',
});
beforeAll(async () => {
await client.connect(transport);
});
test('get-tung-shing', async () => {
const resp = await client.callTool({
name: 'get-tung-shing',
arguments: {
startDate: '2025-01-21'
}
})
expect(resp.content).toBeInstanceOf(Array);
});
```
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g transform="translate(256, 256)">
<g>
<path d="M0,-220 A220,220 0 0,1 0,220 A110,110 0 0,0 0,0 A110,110 0 0,1 0,-220" fill="#111" />
<path d="M0,220 A220,220 0 0,1 0,-220 A110,110 0 0,0 0,0 A110,110 0 0,1 0,220" fill="#fff" />
<circle cx="0" cy="-110" r="35" fill="#fff" />
<circle cx="0" cy="110" r="35" fill="#111" />
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0"
to="360"
dur="24s"
repeatCount="indefinite"
/>
</g>
</g>
</svg>
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install
- uses: changesets/action@v1
id: changesets
with:
publish: pnpm run release
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-tung-shing",
"version": "1.7.1",
"description": "A Model Context Protocol plugin for Chinese Tung Shing (黄历/通勝/通胜) almanac calculations",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"mcp-tung-shing": "./dist/index.cjs"
},
"files": [
"dist"
],
"keywords": [
"mcp",
"tung shing",
"almanac",
"calendar",
"lunar",
"chinese"
],
"repository": {
"type": "git",
"url": "https://github.com/baranwang/mcp-tung-shing"
},
"bugs": {
"url": "https://github.com/baranwang/mcp-tung-shing/issues"
},
"license": "MIT",
"scripts": {
"build": "rslib build",
"check": "biome check --write",
"dev": "rslib build --watch",
"format": "biome format --write",
"inspect": "mcp-inspector",
"pretest": "rslib build",
"test": "vitest run",
"prerelease": "npm run build",
"release": "changeset publish"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"dayjs": "^1.11.13",
"dayjs-plugin-lunar": "^1.4.0",
"tyme4ts": "1.3.4",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.2",
"@changesets/cli": "^2.28.1",
"@modelcontextprotocol/inspector": "^0.8.0",
"@rslib/core": "^0.5.4",
"@types/node": "^22.8.1",
"typescript": "^5.7.3",
"vitest": "^3.0.1"
},
"packageManager": "[email protected]"
}
```
--------------------------------------------------------------------------------
/.github/workflows/update-tyme4ts.yml:
--------------------------------------------------------------------------------
```yaml
name: Update tyme4ts
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-dependency:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install
- name: Check tyme4ts version
id: check-version
run: |
# 获取当前依赖的版本
CURRENT_VERSION=$(node -p "require('./package.json').dependencies.tyme4ts.replace('^', '')")
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
# 获取最新版本
LATEST_VERSION=$(npm view tyme4ts version)
echo "latest_version=$LATEST_VERSION" >> $GITHUB_OUTPUT
# 比较版本
if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then
echo "needs_update=true" >> $GITHUB_OUTPUT
else
echo "needs_update=false" >> $GITHUB_OUTPUT
fi
- name: Update tyme4ts
if: steps.check-version.outputs.needs_update == 'true'
run: |
# 更新依赖版本
pnpm add tyme4ts@${{ steps.check-version.outputs.latest_version }}
# 创建 changeset
cat << EOF > .changeset/update-tyme4ts.md
---
"mcp-tung-shing": patch
---
bump tyme4ts version to ${{ steps.check-version.outputs.latest_version }}
EOF
- name: Commit and push changes
if: steps.check-version.outputs.needs_update == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add package.json pnpm-lock.yaml .changeset/
git commit -m "chore: bump tyme4ts version to ${{ steps.check-version.outputs.latest_version }}"
git push origin main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/src/almanac.ts:
--------------------------------------------------------------------------------
```typescript
import dayjs from 'dayjs';
import { PluginLunar } from 'dayjs-plugin-lunar';
import type { AlmanacContentItem, DailyAlmanac } from './types';
import { ContentType } from './types';
import { handleDirection } from './utils';
import 'dayjs/locale/zh-cn.js';
dayjs.extend(PluginLunar);
/**
* 获取时辰黄历信息
*/
export function getHourlyAlmanac(date: dayjs.Dayjs): AlmanacContentItem {
const lunarHour = date.toLunarHour();
const sixtyCycle = lunarHour.getSixtyCycle();
const heavenStem = sixtyCycle.getHeavenStem();
const earthBranch = sixtyCycle.getEarthBranch();
return {
[ContentType.宜]: lunarHour.getRecommends().map((item) => item.getName()),
[ContentType.忌]: lunarHour.getAvoids().map((item) => item.getName()),
[ContentType.吉凶]: lunarHour
.getTwelveStar()
.getEcliptic()
.getLuck()
.toString(),
[ContentType.值神]: lunarHour.getTwelveStar().toString(),
[ContentType.五行]: sixtyCycle.getSound().toString(),
[ContentType.冲煞]: `冲${earthBranch.getOpposite().getZodiac()}煞${earthBranch.getOminous()}`,
[ContentType.方位]: [
`喜神${handleDirection(heavenStem.getJoyDirection().toString())}`,
`财神${handleDirection(heavenStem.getWealthDirection().toString())}`,
`福神${handleDirection(heavenStem.getMascotDirection().toString())}`,
],
};
}
/**
* 获取每日黄历信息
*/
export function getDailyAlmanac(
date: dayjs.Dayjs,
includeHours = false,
): DailyAlmanac {
const parsedDate = dayjs(date);
if (!parsedDate.isValid()) {
throw new Error('Invalid date');
}
const lunarDay = parsedDate.toLunarDay();
const solarDay = lunarDay.getSolarDay();
const sixtyCycle = lunarDay.getSixtyCycle();
const earthBranch = sixtyCycle.getEarthBranch();
const twentyEightStar = lunarDay.getTwentyEightStar();
const gods = lunarDay.getGods().reduce(
(acc, god) => {
const category =
god.getLuck().getName() === '吉' ? 'auspicious' : 'inauspicious';
acc[category].push(god.getName());
return acc;
},
{ auspicious: [] as string[], inauspicious: [] as string[] },
);
const result: DailyAlmanac = {
公历: parsedDate.locale('zh-cn').format('YYYY 年 M 月 D日(ddd)'),
农历: parsedDate.format('LY年LMLD'),
节日: lunarDay.getFestival()?.getName(),
节气: solarDay.getTermDay().toString(),
七十二候: solarDay.getPhenologyDay().toString(),
当日: {
[ContentType.宜]: lunarDay.getRecommends().map((item) => item.getName()),
[ContentType.忌]: lunarDay.getAvoids().map((item) => item.getName()),
[ContentType.吉凶]: lunarDay
.getTwelveStar()
.getEcliptic()
.getLuck()
.toString(),
[ContentType.五行]: sixtyCycle.getSound().toString(),
[ContentType.冲煞]: `冲${earthBranch.getOpposite().getZodiac()}煞${earthBranch.getOminous()}`,
[ContentType.值神]: lunarDay.getTwelveStar().toString(),
[ContentType.建除十二神]: lunarDay.getDuty().toString(),
[ContentType.二十八星宿]: `${twentyEightStar}${twentyEightStar.getSevenStar()}${twentyEightStar.getAnimal()}(${twentyEightStar.getLuck()})`,
[ContentType.吉神宜趋]: gods.auspicious,
[ContentType.凶煞宜忌]: gods.inauspicious,
[ContentType.彭祖百忌]: `${sixtyCycle.getHeavenStem().getPengZuHeavenStem()} ${earthBranch.getPengZuEarthBranch()}`,
},
};
if (includeHours) {
result.分时 = {};
for (let i = 0; i < 12; i++) {
const hour = parsedDate.addLunar(i, 'dual-hour');
result.分时[hour.format('LH')] = getHourlyAlmanac(hour);
}
}
return result;
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
CallToolRequestSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import dayjs from 'dayjs';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { getDailyAlmanac } from './almanac';
import { ContentType, TabooType, getTungShingParamsSchema } from './types';
import { getDayTabooNames } from './utils';
/**
* 创建并配置MCP服务器
*/
export function createServer() {
const mcpServer = new McpServer(
{
name: 'Tung Shing',
version: process.env.PACKAGE_VERSION ?? '0.0.0',
},
{
capabilities: {
tools: {},
prompts: {},
},
},
);
// 注册工具列表处理器
mcpServer.server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: [
{
name: 'get-tung-shing',
description: '获取通胜黄历,包括公历、农历、宜忌、吉凶、冲煞等信息',
inputSchema: zodToJsonSchema(getTungShingParamsSchema),
},
],
}));
// 注册工具调用处理器
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'get-tung-shing': {
const {
startDate,
days,
includeHours,
tabooFilters = [],
} = getTungShingParamsSchema.parse(request.params.arguments);
const start = dayjs(startDate);
if (!start.isValid()) {
return {
content: [
{
type: 'text',
text: 'Invalid date',
},
],
isError: true,
};
}
return {
content: Array.from({ length: days }, (_, i) => {
const almanac = getDailyAlmanac(start.add(i, 'day'), includeHours);
// 如果没有指定taboo过滤,直接返回结果
if (!tabooFilters.length) {
return {
type: 'text',
text: JSON.stringify(almanac),
};
}
// 提取宜忌内容
const recommends = (almanac.当日[ContentType.宜] as string[]) || [];
const avoids = (almanac.当日[ContentType.忌] as string[]) || [];
// 根据tabooFilters进行过滤,条件之间为或的关系
const hasMatch = tabooFilters.some((filter) => {
// 宜事项过滤
if (filter.type === TabooType.宜) {
return recommends.includes(filter.value);
}
// 忌事项过滤
if (filter.type === TabooType.忌) {
return avoids.includes(filter.value);
}
return false;
});
if (hasMatch) {
return {
type: 'text',
text: JSON.stringify(almanac),
};
}
return null;
}).filter(Boolean),
};
}
default: {
return {
content: [
{
type: 'text',
text: `Unknown tool: ${request.params.name}`,
},
],
isError: true,
};
}
}
});
mcpServer.server.setRequestHandler(ListPromptsRequestSchema, () => ({
prompts: [
{
name: 'get-taboo',
description: '获取宜忌事项类型',
},
],
}));
mcpServer.server.setRequestHandler(GetPromptRequestSchema, (request) => {
switch (request.params.name) {
case 'get-taboo': {
return {
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `宜忌事项类型清单\n${getDayTabooNames()
.map((name) => `- ${name}`)
.join('\n')}`,
},
},
],
};
}
default: {
return {
messages: [],
};
}
}
});
return mcpServer;
}
```