Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ import { API_LIMITS } from './utils/constants.js';

export default class AppleDeveloperDocsMCPServer {
private server: Server;
private isShuttingDown = false;

private shutdown(exitCode: number = 0, reason?: string) {
if (this.isShuttingDown) {
return;
}

this.isShuttingDown = true;
if (reason) {
logger.info(`Shutting down MCP server: ${reason}`);
}
process.exit(exitCode);
}

/**
* Helper method to handle async operations with consistent error handling
Expand Down Expand Up @@ -269,26 +282,38 @@ export default class AppleDeveloperDocsMCPServer {
private setupErrorHandling() {
// 处理 SIGINT 以优雅关闭服务器
process.on('SIGINT', () => {
process.exit(0);
this.shutdown(0, 'SIGINT');
});

process.on('SIGTERM', () => {
process.exit(0);
this.shutdown(0, 'SIGTERM');
});

process.stdin.on('end', () => {
this.shutdown(0, 'stdin end');
});

process.stdin.on('close', () => {
this.shutdown(0, 'stdin close');
});
Comment on lines 284 to 298
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's read the specific lines in question
head -n 310 src/index.ts | tail -n +280 | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 996


🏁 Script executed:

# Get more context around the shutdown setup
head -n 350 src/index.ts | tail -n +270 | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 2740


🏁 Script executed:

# Check the class structure and constructor
head -n 100 src/index.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 4313


🏁 Script executed:

# Check if this class is instantiated multiple times in tests or elsewhere
rg -n "new\s+\w+Server\|new\s+\w+App\|new\s+\w+Manager" --type ts --type tsx

Repository: kimsungwhee/apple-docs-mcp

Length of output: 98


🏁 Script executed:

# Look for any test files that might instantiate this class multiple times
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx | head -20

Repository: kimsungwhee/apple-docs-mcp

Length of output: 845


🏁 Script executed:

# Check the export and how the class is used
rg -n "export.*class|class.*{" src/index.ts -A 3

Repository: kimsungwhee/apple-docs-mcp

Length of output: 178


🏁 Script executed:

# Check basic test file
head -n 100 tests/basic.test.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 1109


🏁 Script executed:

# Check index test file
head -n 100 tests/index.test.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 3701


🏁 Script executed:

# Search for any test setup that creates multiple instances
rg -n "new AppleDeveloperDocsMCPServer" --type ts

Repository: kimsungwhee/apple-docs-mcp

Length of output: 913


🏁 Script executed:

# Check if there are any test fixtures or utilities that instantiate the server
rg -n "AppleDeveloperDocsMCPServer" --type ts -B 2 -A 2

Repository: kimsungwhee/apple-docs-mcp

Length of output: 5333


Use one-shot handlers for process/stdin shutdown events to prevent listener accumulation in tests.

The listeners at lines 284-298 are registered with .on(...) in the constructor. Since this class is instantiated multiple times in test suites (beforeEach patterns in tests/index.test.ts, tests/response-format.test.ts, and regression tests), handlers accumulate on the global process and process.stdin objects even though isShuttingDown prevents duplicate exits. Switching to .once(...) is appropriate since these signals and stdin events should only trigger shutdown once per process lifecycle.

Suggested patch
-    process.on('SIGINT', () => {
+    process.once('SIGINT', () => {
       this.shutdown(0, 'SIGINT');
     });

-    process.on('SIGTERM', () => {
+    process.once('SIGTERM', () => {
       this.shutdown(0, 'SIGTERM');
     });

-    process.stdin.on('end', () => {
+    process.stdin.once('end', () => {
       this.shutdown(0, 'stdin end');
     });

-    process.stdin.on('close', () => {
+    process.stdin.once('close', () => {
       this.shutdown(0, 'stdin close');
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.on('SIGINT', () => {
process.exit(0);
this.shutdown(0, 'SIGINT');
});
process.on('SIGTERM', () => {
process.exit(0);
this.shutdown(0, 'SIGTERM');
});
process.stdin.on('end', () => {
this.shutdown(0, 'stdin end');
});
process.stdin.on('close', () => {
this.shutdown(0, 'stdin close');
});
process.once('SIGINT', () => {
this.shutdown(0, 'SIGINT');
});
process.once('SIGTERM', () => {
this.shutdown(0, 'SIGTERM');
});
process.stdin.once('end', () => {
this.shutdown(0, 'stdin end');
});
process.stdin.once('close', () => {
this.shutdown(0, 'stdin close');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 284 - 298, Replace the persistent listeners with
one-shot handlers: change the process and stdin event registrations that
currently call process.on(...) for 'SIGINT', 'SIGTERM', and the 'end'/'close'
listeners on process.stdin to use .once(...) so each event invokes the class's
shutdown(...) handler only once per process lifecycle; update the registrations
around the shutdown(...) references (the SIGINT, SIGTERM, process.stdin 'end'
and 'close' handlers) accordingly to prevent listener accumulation during
repeated test instantiation.


process.on('unhandledRejection', (reason) => {
logger.error('Unhandled Rejection, reason:', reason);
process.exit(1);
this.shutdown(1, 'unhandledRejection');
});

process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
this.shutdown(1, 'uncaughtException');
});
}

async run() {
const transport = new StdioServerTransport();
transport.onclose = () => {
this.shutdown(0, 'transport close');
};

await this.server.connect(transport);

logger.info('Apple Developer Docs MCP server running on stdio');
Expand Down Expand Up @@ -316,4 +341,4 @@ if (process.env.NODE_ENV !== 'test') {
logger.error('Fatal error in main():', error);
process.exit(1);
});
}
}