#
tokens: 5654/50000 20/20 files
lines: off (toggle) GitHub
raw markdown copy
# 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

[![smithery badge](https://smithery.ai/badge/@baranwang/mcp-tung-shing)](https://smithery.ai/server/@baranwang/mcp-tung-shing)
[![NPM Version](https://img.shields.io/npm/v/mcp-tung-shing.svg)](https://www.npmjs.com/package/mcp-tung-shing)
[![License](https://img.shields.io/npm/l/mcp-tung-shing.svg)](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;
}

```