Skip to content

Commit a6b4433

Browse files
committed
feat: implement MCP logging/setLevel method handler
- Add logging/setLevel request handler to fix VSCode MCP protocol error - Enhance logger with client-controlled log level filtering - Support MCP log levels: emergency, alert, critical, error, warning, notice, info, debug - Maintain test suppression logic for clean test output and doctor-cli - Export setLogLevel() and getLogLevel() functions for MCP integration - Return empty result as per MCP specification requirements Fixes MCP -32601 "Method not found" error when VSCode tries to set logging levels.
1 parent bc68a81 commit a6b4433

File tree

3 files changed

+86
-18
lines changed

3 files changed

+86
-18
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ A Model Context Protocol (MCP) server that provides Xcode-related tools for inte
2929
- [Dynamic Tools](#dynamic-tools)
3030
- [What is Dynamic Tools?](#what-is-dynamic-tools)
3131
- [How to Enable Dynamic Tools](#how-to-enable-dynamic-tools)
32+
- [Selective Workflow Loading (Static Mode)](#selective-workflow-loading-static-mode)
3233
- [Usage Example](#usage-example)
3334
- [Client Compatibility](#client-compatibility)
3435
- [Code Signing for Device Deployment](#code-signing-for-device-deployment)
@@ -79,7 +80,7 @@ The XcodeBuildMCP server provides the following tool capabilities:
7980
- **Clean Artifacts**: Remove build artifacts and derived data for fresh builds
8081

8182
### Simulator management
82-
- **Simulator Control**: List, boot, and open simulators
83+
- **Simulator Control**: List, boot, and open simulators
8384
- **App Lifecycle**: Complete app management - install, launch, and stop apps on simulators
8485
- **Log Capture**: Capture run-time logs from a simulator
8586
- **UI Automation**: Interact with simulator UI elements
@@ -121,7 +122,7 @@ For clients that support MCP resources XcodeBuildMCP provides efficient URI-base
121122

122123
For a quick install, you can use the following links:
123124

124-
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=XcodeBuildMCP&config=eyJjb21tYW5kIjoibnB4IC15IHhjb2RlYnVpbGRtY3BAbGF0ZXN0In0%3D)
125+
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=XcodeBuildMCP&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IC15IHhjb2RlYnVpbGRtY3BAbGF0ZXN0IiwiZW52Ijp7IklOQ1JFTUVOVEFMX0JVSUxEU19FTkFCTEVEIjoiZmFsc2UiLCJYQ09ERUJVSUxETUNQX1NFTlRSWV9ESVNBQkxFRCI6ImZhbHNlIn19)
125126
- [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D)
126127
- [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect/mcp/install?name=XcodeBuildMCP&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22xcodebuildmcp%40latest%22%5D%7D&quality=insiders)
127128

@@ -177,7 +178,7 @@ Then configure your MCP client to use mise to install XcodeBuildMCP:
177178
> When using mise avoid using the @latest tag as mise will cache the package and may not update to the latest version automatically, instead prefer an explicit version number.
178179
179180
> [!IMPORTANT]
180-
> Please note that XcodeBuildMCP will request xcodebuild to skip macro validation. This is to avoid errors when building projects that use Swift Macros.
181+
> Please note that XcodeBuildMCP will request xcodebuild to skip macro validation. This is to avoid errors when building projects that use Swift Macros.
181182
182183
#### Installing via Smithery
183184

@@ -194,7 +195,7 @@ XcodeBuildMCP supports both MCP tools, resources and sampling. At time of writin
194195
| Editor | Tools | Resources | Samplng |
195196
|--------|-------|-----------|---------|
196197
| **VS Code** ||||
197-
| **Cursor** ||||
198+
| **Cursor** ||||
198199
| **Windsurf** ||||
199200
| **Claude Code** ||||
200201
| **Claude Desktop** ||||
@@ -217,7 +218,7 @@ Example MCP client configuration:
217218
],
218219
"env": {
219220
"INCREMENTAL_BUILDS_ENABLED": "true"
220-
}
221+
}
221222
}
222223
}
223224
}
@@ -255,7 +256,7 @@ Example MCP client configuration:
255256
],
256257
"env": {
257258
"XCODEBUILDMCP_DYNAMIC_TOOLS": "true"
258-
}
259+
}
259260
}
260261
}
261262
}
@@ -276,7 +277,7 @@ For clients that don't support MCP Sampling but still want to reduce context win
276277
],
277278
"env": {
278279
"XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,device,project-discovery"
279-
}
280+
}
280281
}
281282
}
282283
}
@@ -362,7 +363,7 @@ This project uses [Sentry](https://sentry.io/) for error monitoring and diagnost
362363
- Error logs may include details such as error messages, stack traces, and (in some cases) file paths or project names. You can review the sources in this repository to see exactly what is logged.
363364

364365
### Opting Out of Sentry
365-
- If you do not wish to send error logs to Sentry, you can opt out by setting the environment variable `SENTRY_DISABLED=true`.
366+
- If you do not wish to send error logs to Sentry, you can opt out by setting the environment variable `XCODEBUILDMCP_SENTRY_DISABLED=true`.
366367

367368
Example MCP client configuration:
368369
```json
@@ -375,8 +376,8 @@ Example MCP client configuration:
375376
"xcodebuildmcp@latest"
376377
],
377378
"env": {
378-
"SENTRY_DISABLED": "true"
379-
}
379+
"XCODEBUILDMCP_SENTRY_DISABLED": "true"
380+
}
380381
}
381382
}
382383
}

src/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ import './utils/sentry.ts';
2020
import { createServer, startServer } from './server/server.ts';
2121
import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';
2222

23+
// Import MCP types for logging
24+
import { SetLevelRequestSchema } from '@camsoft/mcp-sdk/types.js';
25+
2326
// Import utilities
24-
import { log } from './utils/logger.ts';
27+
import { log, setLogLevel, type LogLevel } from './utils/logger.ts';
2528

2629
// Import version
2730
import { version } from './version.ts';
@@ -64,6 +67,14 @@ async function main(): Promise<void> {
6467
// Create the server
6568
const server = createServer();
6669

70+
// Register logging/setLevel handler
71+
server.server.setRequestHandler(SetLevelRequestSchema, async (request) => {
72+
const { level } = request.params;
73+
setLogLevel(level as LogLevel);
74+
log('info', `Client requested log level: ${level}`);
75+
return {}; // Empty result as per MCP spec
76+
});
77+
6778
// Make server available globally for dynamic tools
6879
(globalThis as { mcpServer?: McpServer }).mcpServer = server;
6980

src/utils/logger.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ import { createRequire } from 'node:module';
2323
const SENTRY_ENABLED =
2424
process.env.SENTRY_DISABLED !== 'true' && process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
2525

26+
// Log levels in order of severity (lower number = more severe)
27+
const LOG_LEVELS = {
28+
emergency: 0,
29+
alert: 1,
30+
critical: 2,
31+
error: 3,
32+
warning: 4,
33+
notice: 5,
34+
info: 6,
35+
debug: 7,
36+
} as const;
37+
38+
export type LogLevel = keyof typeof LOG_LEVELS;
39+
40+
// Client-requested log level (null means no filtering)
41+
let clientLogLevel: LogLevel | null = null;
42+
2643
function isTestEnv(): boolean {
2744
return (
2845
process.env.VITEST === 'true' ||
@@ -66,18 +83,57 @@ if (!SENTRY_ENABLED) {
6683
}
6784
}
6885

86+
/**
87+
* Set the minimum log level for client-requested filtering
88+
* @param level The minimum log level to output
89+
*/
90+
export function setLogLevel(level: LogLevel): void {
91+
clientLogLevel = level;
92+
log('debug', `Log level set to: ${level}`);
93+
}
94+
95+
/**
96+
* Get the current client-requested log level
97+
* @returns The current log level or null if no filtering is active
98+
*/
99+
export function getLogLevel(): LogLevel | null {
100+
return clientLogLevel;
101+
}
102+
103+
/**
104+
* Check if a log level should be output based on client settings
105+
* @param level The log level to check
106+
* @returns true if the message should be logged
107+
*/
108+
function shouldLog(level: string): boolean {
109+
// Suppress logging during tests to keep test output clean
110+
if (isTestEnv()) {
111+
return false;
112+
}
113+
114+
// If no client level set, log everything
115+
if (clientLogLevel === null) {
116+
return true;
117+
}
118+
119+
// Check if the level is valid
120+
const levelKey = level.toLowerCase() as LogLevel;
121+
if (!(levelKey in LOG_LEVELS)) {
122+
return true; // Log unknown levels
123+
}
124+
125+
// Only log if the message level is at or above the client's requested level
126+
return LOG_LEVELS[levelKey] <= LOG_LEVELS[clientLogLevel];
127+
}
128+
69129
/**
70130
* Log a message with the specified level
71-
* @param level The log level (info, warning, error, debug)
131+
* @param level The log level (emergency, alert, critical, error, warning, notice, info, debug)
72132
* @param message The message to log
73133
*/
74134
export function log(level: string, message: string): void {
75-
// Suppress logging during tests to keep test output clean
76-
if (
77-
process.env.VITEST === 'true' ||
78-
process.env.NODE_ENV === 'test' ||
79-
process.env.XCODEBUILDMCP_SILENCE_LOGS === 'true'
80-
) {
135+
// Check if we should log this level
136+
if (!shouldLog(level)) {
81137
return;
82138
}
83139

0 commit comments

Comments
 (0)