@xzkcz/iztro-chart-mcp-server 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/astrolabe.d.ts +7 -0
- package/dist/astrolabe.js +48 -0
- package/dist/astrolabe.test.d.ts +1 -0
- package/dist/astrolabe.test.js +228 -0
- package/dist/hourToTimeIndex.d.ts +7 -0
- package/dist/hourToTimeIndex.js +64 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +51 -0
- package/dist/version.json +3 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 iztro-mcp-server
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# iztro Chart MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for generating and capturing astrolabe charts from ziwei.pub using FastMCP and Playwright.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **FastMCP Integration**: Built with the FastMCP TypeScript framework for easy MCP server development
|
|
8
|
+
- **Astrolabe Screenshot Capture**: Automatically captures screenshots of astrolabe charts from ziwei.pub
|
|
9
|
+
- **Playwright Automation**: Uses Playwright for reliable web scraping and screenshot generation
|
|
10
|
+
- **TypeScript Support**: Full TypeScript support with proper type definitions
|
|
11
|
+
- **Parameter Validation**: Uses Zod for robust parameter validation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @xzkcz/iztro-chart-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Building
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Running
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Running with npx
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @xzkcz/iztro-chart-mcp-server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Testing with MCP CLI
|
|
44
|
+
|
|
45
|
+
You can test the server using the MCP CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Install MCP CLI if you haven't already
|
|
49
|
+
npm install -g @modelcontextprotocol/cli
|
|
50
|
+
|
|
51
|
+
# Test the server
|
|
52
|
+
npx @mcp/cli src/index.ts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Available Tools
|
|
56
|
+
|
|
57
|
+
### 1. `download_chart`
|
|
58
|
+
Capture a screenshot of an astrolabe chart from ziwei.pub.
|
|
59
|
+
|
|
60
|
+
**Parameters:**
|
|
61
|
+
- `date` (string): Birth date in YYYY-M-D format (e.g., "2000-8-16")
|
|
62
|
+
- `hour` (number): Birth hour in 24-hour format (0-23)
|
|
63
|
+
- `gender` (enum): Gender of the person ("male" or "female")
|
|
64
|
+
- `path` (string, optional): Path to save image (default: "astro-chart.png")
|
|
65
|
+
|
|
66
|
+
**Example:**
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"name": "download_chart",
|
|
70
|
+
"arguments": {
|
|
71
|
+
"date": "2000-8-16",
|
|
72
|
+
"hour": 6,
|
|
73
|
+
"gender": "male",
|
|
74
|
+
"path": "my-chart.png"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Claude Desktop Integration
|
|
80
|
+
|
|
81
|
+
To use this server with Claude Desktop, add the following to your Claude Desktop configuration:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"iztro-chart": {
|
|
87
|
+
"command": "npx",
|
|
88
|
+
"args": ["@xzkcz/iztro-chart-mcp-server"],
|
|
89
|
+
"env": {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Project Structure
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
├── src/
|
|
99
|
+
│ ├── index.ts # Main server implementation
|
|
100
|
+
│ ├── astrolabe.ts # Astrolabe screenshot logic
|
|
101
|
+
│ ├── astrolabe.test.ts # Unit tests for astrolabe functionality
|
|
102
|
+
│ └── hourToTimeIndex.ts # Hour to Chinese time index conversion
|
|
103
|
+
├── dist/ # Compiled JavaScript output
|
|
104
|
+
├── package.json # Project dependencies and scripts
|
|
105
|
+
├── tsconfig.json # TypeScript configuration
|
|
106
|
+
├── vitest.config.ts # Vitest test configuration
|
|
107
|
+
└── README.md # This file
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Dependencies
|
|
111
|
+
|
|
112
|
+
- **@modelcontextprotocol/sdk**: Core MCP SDK
|
|
113
|
+
- **fastmcp**: FastMCP framework for TypeScript
|
|
114
|
+
- **playwright**: Web automation and screenshot capture
|
|
115
|
+
- **zod**: Schema validation
|
|
116
|
+
- **typescript**: TypeScript compiler
|
|
117
|
+
- **tsx**: TypeScript execution engine
|
|
118
|
+
|
|
119
|
+
### Development Dependencies
|
|
120
|
+
|
|
121
|
+
- **vitest**: Fast unit testing framework
|
|
122
|
+
- **@vitest/ui**: Visual test interface
|
|
123
|
+
- **jsdom**: DOM implementation for testing
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { hourToTimeIndex } from "./hourToTimeIndex.js";
|
|
5
|
+
export async function downloadChart(params) {
|
|
6
|
+
const { date, hour, gender, path: savePath } = params;
|
|
7
|
+
// Convert hour to timeIndex for iztro library
|
|
8
|
+
const timeIndex = hourToTimeIndex(hour);
|
|
9
|
+
// Construct the URL
|
|
10
|
+
const url = `https://ziwei.pub/astrolabe/?d=${date}&t=${timeIndex}&leap=false&g=${gender}&type=solar&n=`;
|
|
11
|
+
let browser;
|
|
12
|
+
try {
|
|
13
|
+
// Launch browser
|
|
14
|
+
browser = await chromium.launch({ headless: true });
|
|
15
|
+
const context = await browser.newContext({
|
|
16
|
+
viewport: { width: 1500, height: 800 }
|
|
17
|
+
});
|
|
18
|
+
const page = await context.newPage();
|
|
19
|
+
// Navigate to the astrolabe page
|
|
20
|
+
await page.goto(url);
|
|
21
|
+
// Wait for the astrolabe element to be visible
|
|
22
|
+
await page.waitForSelector('.iztro-astrolabe', { timeout: 3000 });
|
|
23
|
+
// Take screenshot of the specific element
|
|
24
|
+
const targetSelector = ".iztro-astrolabe";
|
|
25
|
+
const element = await page.locator(targetSelector);
|
|
26
|
+
// Ensure the element is found
|
|
27
|
+
if (!element) {
|
|
28
|
+
throw new Error(`Element ${targetSelector} not found`);
|
|
29
|
+
}
|
|
30
|
+
// Ensure the directory exists
|
|
31
|
+
const fullPath = path.resolve(savePath);
|
|
32
|
+
const dir = path.dirname(fullPath);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
// Take the screenshot
|
|
37
|
+
await element.screenshot({ path: fullPath });
|
|
38
|
+
await browser.close();
|
|
39
|
+
return `Astrolabe chart downloaded successfully to ${fullPath}. URL: ${url}. Parameters: date=${date}, hour=${hour}, gender=${gender}`;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (browser) {
|
|
43
|
+
await browser.close();
|
|
44
|
+
}
|
|
45
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
46
|
+
return `Failed to download astrolabe chart: ${errorMessage}. URL: ${url}. Parameters: date=${date}, hour=${hour}, gender=${gender}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { downloadChart } from './astrolabe.js';
|
|
3
|
+
import { chromium } from 'playwright';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
// Mock Playwright
|
|
7
|
+
vi.mock('playwright', () => ({
|
|
8
|
+
chromium: {
|
|
9
|
+
launch: vi.fn()
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
// Mock fs
|
|
13
|
+
vi.mock('fs', () => ({
|
|
14
|
+
existsSync: vi.fn(),
|
|
15
|
+
mkdirSync: vi.fn()
|
|
16
|
+
}));
|
|
17
|
+
// Mock path
|
|
18
|
+
vi.mock('path', () => ({
|
|
19
|
+
resolve: vi.fn(),
|
|
20
|
+
dirname: vi.fn()
|
|
21
|
+
}));
|
|
22
|
+
describe('downloadChart', () => {
|
|
23
|
+
const mockBrowser = {
|
|
24
|
+
newContext: vi.fn(),
|
|
25
|
+
close: vi.fn()
|
|
26
|
+
};
|
|
27
|
+
const mockContext = {
|
|
28
|
+
newPage: vi.fn()
|
|
29
|
+
};
|
|
30
|
+
const mockPage = {
|
|
31
|
+
goto: vi.fn(),
|
|
32
|
+
waitForSelector: vi.fn(),
|
|
33
|
+
locator: vi.fn()
|
|
34
|
+
};
|
|
35
|
+
const mockElement = {
|
|
36
|
+
screenshot: vi.fn()
|
|
37
|
+
};
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
// Setup default mock implementations
|
|
41
|
+
vi.mocked(chromium.launch).mockResolvedValue(mockBrowser);
|
|
42
|
+
mockBrowser.newContext.mockResolvedValue(mockContext);
|
|
43
|
+
mockContext.newPage.mockResolvedValue(mockPage);
|
|
44
|
+
mockPage.locator.mockReturnValue(mockElement);
|
|
45
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
46
|
+
vi.mocked(path.resolve).mockImplementation((p) => `/resolved/${p}`);
|
|
47
|
+
vi.mocked(path.dirname).mockImplementation((p) => `/dirname/${p}`);
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
describe('successful chart download', () => {
|
|
53
|
+
it('should successfully download chart with valid parameters', async () => {
|
|
54
|
+
const params = {
|
|
55
|
+
date: '2000-8-16',
|
|
56
|
+
hour: 6,
|
|
57
|
+
gender: 'male',
|
|
58
|
+
path: 'test-chart.png'
|
|
59
|
+
};
|
|
60
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
61
|
+
const result = await downloadChart(params);
|
|
62
|
+
expect(chromium.launch).toHaveBeenCalledWith({ headless: true });
|
|
63
|
+
expect(mockBrowser.newContext).toHaveBeenCalledWith({
|
|
64
|
+
viewport: { width: 1500, height: 800 }
|
|
65
|
+
});
|
|
66
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://ziwei.pub/astrolabe/?d=2000-8-16&t=3&leap=false&g=male&type=solar&n=');
|
|
67
|
+
expect(mockPage.waitForSelector).toHaveBeenCalledWith('.iztro-astrolabe', { timeout: 3000 });
|
|
68
|
+
expect(mockPage.locator).toHaveBeenCalledWith('.iztro-astrolabe');
|
|
69
|
+
expect(mockElement.screenshot).toHaveBeenCalledWith({ path: '/resolved/test-chart.png' });
|
|
70
|
+
expect(mockBrowser.close).toHaveBeenCalled();
|
|
71
|
+
expect(result).toContain('Astrolabe chart downloaded successfully');
|
|
72
|
+
expect(result).toContain('/resolved/test-chart.png');
|
|
73
|
+
});
|
|
74
|
+
it('should create directory if it does not exist', async () => {
|
|
75
|
+
const params = {
|
|
76
|
+
date: '2000-8-16',
|
|
77
|
+
hour: 6,
|
|
78
|
+
gender: 'female',
|
|
79
|
+
path: 'charts/test-chart.png'
|
|
80
|
+
};
|
|
81
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
82
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
83
|
+
await downloadChart(params);
|
|
84
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith('/dirname//resolved/charts/test-chart.png', { recursive: true });
|
|
85
|
+
});
|
|
86
|
+
it('should handle female gender parameter correctly', async () => {
|
|
87
|
+
const params = {
|
|
88
|
+
date: '1995-12-25',
|
|
89
|
+
hour: 0,
|
|
90
|
+
gender: 'female',
|
|
91
|
+
path: 'female-chart.png'
|
|
92
|
+
};
|
|
93
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
94
|
+
const result = await downloadChart(params);
|
|
95
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://ziwei.pub/astrolabe/?d=1995-12-25&t=0&leap=false&g=female&type=solar&n=');
|
|
96
|
+
expect(result).toContain('gender=female');
|
|
97
|
+
});
|
|
98
|
+
it('should handle edge case hour values', async () => {
|
|
99
|
+
const params = {
|
|
100
|
+
date: '2023-1-1',
|
|
101
|
+
hour: 12,
|
|
102
|
+
gender: 'male',
|
|
103
|
+
path: 'edge-chart.png'
|
|
104
|
+
};
|
|
105
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
106
|
+
const result = await downloadChart(params);
|
|
107
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://ziwei.pub/astrolabe/?d=2023-1-1&t=6&leap=false&g=male&type=solar&n=');
|
|
108
|
+
expect(result).toContain('hour=12');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('error handling', () => {
|
|
112
|
+
it('should handle browser launch failure', async () => {
|
|
113
|
+
const params = {
|
|
114
|
+
date: '2000-8-16',
|
|
115
|
+
hour: 6,
|
|
116
|
+
gender: 'male',
|
|
117
|
+
path: 'test-chart.png'
|
|
118
|
+
};
|
|
119
|
+
const error = new Error('Failed to launch browser');
|
|
120
|
+
vi.mocked(chromium.launch).mockRejectedValue(error);
|
|
121
|
+
const result = await downloadChart(params);
|
|
122
|
+
expect(result).toContain('Failed to download astrolabe chart: Failed to launch browser');
|
|
123
|
+
expect(result).toContain('date=2000-8-16');
|
|
124
|
+
expect(result).toContain('hour=6');
|
|
125
|
+
expect(result).toContain('gender=male');
|
|
126
|
+
});
|
|
127
|
+
it('should handle page navigation failure', async () => {
|
|
128
|
+
const params = {
|
|
129
|
+
date: '2000-8-16',
|
|
130
|
+
hour: 6,
|
|
131
|
+
gender: 'male',
|
|
132
|
+
path: 'test-chart.png'
|
|
133
|
+
};
|
|
134
|
+
const error = new Error('Navigation timeout');
|
|
135
|
+
mockPage.goto.mockRejectedValue(error);
|
|
136
|
+
const result = await downloadChart(params);
|
|
137
|
+
expect(mockBrowser.close).toHaveBeenCalled();
|
|
138
|
+
expect(result).toContain('Failed to download astrolabe chart: Navigation timeout');
|
|
139
|
+
});
|
|
140
|
+
it('should handle selector timeout', async () => {
|
|
141
|
+
const params = {
|
|
142
|
+
date: '2000-8-16',
|
|
143
|
+
hour: 6,
|
|
144
|
+
gender: 'male',
|
|
145
|
+
path: 'test-chart.png'
|
|
146
|
+
};
|
|
147
|
+
const error = new Error('Timeout waiting for selector');
|
|
148
|
+
mockPage.waitForSelector.mockRejectedValue(error);
|
|
149
|
+
const result = await downloadChart(params);
|
|
150
|
+
expect(mockBrowser.close).toHaveBeenCalled();
|
|
151
|
+
expect(result).toContain('Failed to download astrolabe chart: Timeout waiting for selector');
|
|
152
|
+
});
|
|
153
|
+
it('should handle screenshot failure', async () => {
|
|
154
|
+
const params = {
|
|
155
|
+
date: '2000-8-16',
|
|
156
|
+
hour: 6,
|
|
157
|
+
gender: 'male',
|
|
158
|
+
path: 'test-chart.png'
|
|
159
|
+
};
|
|
160
|
+
const error = new Error('Screenshot failed');
|
|
161
|
+
mockElement.screenshot.mockRejectedValue(error);
|
|
162
|
+
const result = await downloadChart(params);
|
|
163
|
+
expect(mockBrowser.close).toHaveBeenCalled();
|
|
164
|
+
expect(result).toContain('Failed to download astrolabe chart: Screenshot failed');
|
|
165
|
+
});
|
|
166
|
+
it('should handle non-Error exceptions', async () => {
|
|
167
|
+
const params = {
|
|
168
|
+
date: '2000-8-16',
|
|
169
|
+
hour: 6,
|
|
170
|
+
gender: 'male',
|
|
171
|
+
path: 'test-chart.png'
|
|
172
|
+
};
|
|
173
|
+
vi.mocked(chromium.launch).mockRejectedValue('String error');
|
|
174
|
+
const result = await downloadChart(params);
|
|
175
|
+
expect(result).toContain('Failed to download astrolabe chart: String error');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('URL construction', () => {
|
|
179
|
+
it('should construct correct URL with all parameters', async () => {
|
|
180
|
+
const params = {
|
|
181
|
+
date: '1990-5-20',
|
|
182
|
+
hour: 8,
|
|
183
|
+
gender: 'female',
|
|
184
|
+
path: 'url-test.png'
|
|
185
|
+
};
|
|
186
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
187
|
+
await downloadChart(params);
|
|
188
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://ziwei.pub/astrolabe/?d=1990-5-20&t=4&leap=false&g=female&type=solar&n=');
|
|
189
|
+
});
|
|
190
|
+
it('should handle special characters in date', async () => {
|
|
191
|
+
const params = {
|
|
192
|
+
date: '2000-12-31',
|
|
193
|
+
hour: 11,
|
|
194
|
+
gender: 'male',
|
|
195
|
+
path: 'special-date.png'
|
|
196
|
+
};
|
|
197
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
198
|
+
await downloadChart(params);
|
|
199
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://ziwei.pub/astrolabe/?d=2000-12-31&t=6&leap=false&g=male&type=solar&n=');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('file system operations', () => {
|
|
203
|
+
it('should resolve path correctly', async () => {
|
|
204
|
+
const params = {
|
|
205
|
+
date: '2000-8-16',
|
|
206
|
+
hour: 6,
|
|
207
|
+
gender: 'male',
|
|
208
|
+
path: 'relative/path/chart.png'
|
|
209
|
+
};
|
|
210
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
211
|
+
await downloadChart(params);
|
|
212
|
+
expect(path.resolve).toHaveBeenCalledWith('relative/path/chart.png');
|
|
213
|
+
expect(mockElement.screenshot).toHaveBeenCalledWith({ path: '/resolved/relative/path/chart.png' });
|
|
214
|
+
});
|
|
215
|
+
it('should check directory existence', async () => {
|
|
216
|
+
const params = {
|
|
217
|
+
date: '2000-8-16',
|
|
218
|
+
hour: 6,
|
|
219
|
+
gender: 'male',
|
|
220
|
+
path: 'test/chart.png'
|
|
221
|
+
};
|
|
222
|
+
mockElement.screenshot.mockResolvedValue(undefined);
|
|
223
|
+
await downloadChart(params);
|
|
224
|
+
expect(path.dirname).toHaveBeenCalledWith('/resolved/test/chart.png');
|
|
225
|
+
expect(fs.existsSync).toHaveBeenCalledWith('/dirname//resolved/test/chart.png');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert 24-hour format to Chinese timeIndex (0-12)
|
|
3
|
+
* Based on traditional Chinese time periods (十二时辰)
|
|
4
|
+
* @param hour - Hour in 24-hour format (0-23)
|
|
5
|
+
* @returns timeIndex - Chinese hour index (0-12)
|
|
6
|
+
*/
|
|
7
|
+
export declare function hourToTimeIndex(hour: number): number;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert 24-hour format to Chinese timeIndex (0-12)
|
|
3
|
+
* Based on traditional Chinese time periods (十二时辰)
|
|
4
|
+
* @param hour - Hour in 24-hour format (0-23)
|
|
5
|
+
* @returns timeIndex - Chinese hour index (0-12)
|
|
6
|
+
*/
|
|
7
|
+
export function hourToTimeIndex(hour) {
|
|
8
|
+
if (hour < 0 || hour > 23 || !Number.isInteger(hour)) {
|
|
9
|
+
throw new Error('Hour must be an integer between 0 and 23');
|
|
10
|
+
}
|
|
11
|
+
// Chinese time periods mapping (十二时辰) with early/late Zi distinction:
|
|
12
|
+
// 早子时 (Early Zi): 00:00-01:00 -> timeIndex 0
|
|
13
|
+
// 丑时 (Chou): 01:00-03:00 -> timeIndex 1
|
|
14
|
+
// 寅时 (Yin): 03:00-05:00 -> timeIndex 2
|
|
15
|
+
// 卯时 (Mao): 05:00-07:00 -> timeIndex 3
|
|
16
|
+
// 辰时 (Chen): 07:00-09:00 -> timeIndex 4
|
|
17
|
+
// 巳时 (Si): 09:00-11:00 -> timeIndex 5
|
|
18
|
+
// 午时 (Wu): 11:00-13:00 -> timeIndex 6
|
|
19
|
+
// 未时 (Wei): 13:00-15:00 -> timeIndex 7
|
|
20
|
+
// 申时 (Shen): 15:00-17:00 -> timeIndex 8
|
|
21
|
+
// 酉时 (You): 17:00-19:00 -> timeIndex 9
|
|
22
|
+
// 戌时 (Xu): 19:00-21:00 -> timeIndex 10
|
|
23
|
+
// 亥时 (Hai): 21:00-23:00 -> timeIndex 11
|
|
24
|
+
// 晚子时 (Late Zi): 23:00-00:00 -> timeIndex 12
|
|
25
|
+
if (hour >= 0 && hour < 1) {
|
|
26
|
+
return 0; // 早子时 (Early Zi) - 00:00-01:00
|
|
27
|
+
}
|
|
28
|
+
else if (hour >= 1 && hour < 3) {
|
|
29
|
+
return 1; // 丑时 (Chou) - 01:00-03:00
|
|
30
|
+
}
|
|
31
|
+
else if (hour >= 3 && hour < 5) {
|
|
32
|
+
return 2; // 寅时 (Yin) - 03:00-05:00
|
|
33
|
+
}
|
|
34
|
+
else if (hour >= 5 && hour < 7) {
|
|
35
|
+
return 3; // 卯时 (Mao) - 05:00-07:00
|
|
36
|
+
}
|
|
37
|
+
else if (hour >= 7 && hour < 9) {
|
|
38
|
+
return 4; // 辰时 (Chen) - 07:00-09:00
|
|
39
|
+
}
|
|
40
|
+
else if (hour >= 9 && hour < 11) {
|
|
41
|
+
return 5; // 巳时 (Si) - 09:00-11:00
|
|
42
|
+
}
|
|
43
|
+
else if (hour >= 11 && hour < 13) {
|
|
44
|
+
return 6; // 午时 (Wu) - 11:00-13:00
|
|
45
|
+
}
|
|
46
|
+
else if (hour >= 13 && hour < 15) {
|
|
47
|
+
return 7; // 未时 (Wei) - 13:00-15:00
|
|
48
|
+
}
|
|
49
|
+
else if (hour >= 15 && hour < 17) {
|
|
50
|
+
return 8; // 申时 (Shen) - 15:00-17:00
|
|
51
|
+
}
|
|
52
|
+
else if (hour >= 17 && hour < 19) {
|
|
53
|
+
return 9; // 酉时 (You) - 17:00-19:00
|
|
54
|
+
}
|
|
55
|
+
else if (hour >= 19 && hour < 21) {
|
|
56
|
+
return 10; // 戌时 (Xu) - 19:00-21:00
|
|
57
|
+
}
|
|
58
|
+
else if (hour >= 21 && hour < 23) {
|
|
59
|
+
return 11; // 亥时 (Hai) - 21:00-23:00
|
|
60
|
+
}
|
|
61
|
+
else { // hour == 23
|
|
62
|
+
return 12; // 晚子时 (Late Zi) - 23:00-00:00
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { FastMCP } from "fastmcp";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { downloadChart } from "./astrolabe.js";
|
|
5
|
+
import versionJson from './version.json' with { type: 'json' };
|
|
6
|
+
const VERSION = versionJson.version;
|
|
7
|
+
// Create a new FastMCP server instance
|
|
8
|
+
const server = new FastMCP({
|
|
9
|
+
name: "@xzkcz/iztro-chart-mcp-server",
|
|
10
|
+
version: VERSION
|
|
11
|
+
});
|
|
12
|
+
// Add astrolabe chart download tool
|
|
13
|
+
server.addTool({
|
|
14
|
+
name: "download_chart",
|
|
15
|
+
description: "Download a screenshot of an astrolabe chart from ziwei.pub",
|
|
16
|
+
parameters: z.object({
|
|
17
|
+
date: z.string().describe('Birth date in YYYY-M-D format (e.g., "2000-8-16")'),
|
|
18
|
+
hour: z.number().min(0).max(23).describe('Birth hour in 24-hour format (0-23)'),
|
|
19
|
+
gender: z.enum(['male', 'female']).describe('Gender of the person'),
|
|
20
|
+
path: z.string().optional().default('chart.png').describe('Path to save image')
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args) => {
|
|
23
|
+
return await downloadChart(args);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Start the server
|
|
27
|
+
async function main() {
|
|
28
|
+
try {
|
|
29
|
+
server.start({
|
|
30
|
+
transportType: "stdio",
|
|
31
|
+
});
|
|
32
|
+
console.error(`🌟iztro-chart-mcp-server@${VERSION} started successfully!`);
|
|
33
|
+
console.error('Available tools:');
|
|
34
|
+
console.error(' - download_chart: Download a screenshot of an astrolabe chart from ziwei.pub');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Server error:", error);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Handle graceful shutdown
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
console.log('\nShutting down server...');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
console.log('\nShutting down server...');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
// Start the server
|
|
51
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xzkcz/iztro-chart-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "An MCP server for iZtro astrology chart generation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"iztro-chart-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prebuild": "node scripts/generate-version.js",
|
|
17
|
+
"build": "npm run prebuild && tsc",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:run": "vitest run",
|
|
22
|
+
"test:ui": "vitest --ui",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"clean": "rm -rf dist",
|
|
25
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
26
|
+
"prepack": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"astrology",
|
|
31
|
+
"iztro",
|
|
32
|
+
"chart",
|
|
33
|
+
"server"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
39
|
+
"fastmcp": "^3.19.0",
|
|
40
|
+
"playwright": "^1.55.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.6.2",
|
|
44
|
+
"@vitest/ui": "^3.2.4",
|
|
45
|
+
"jsdom": "^27.0.0",
|
|
46
|
+
"tsx": "^4.20.6",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/xzkcz/iztro-chart-mcp-server.git"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/xzkcz/iztro-chart-mcp-server#readme",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/xzkcz/iztro-chart-mcp-server/issues"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18.0.0"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|