Skip to content

Commit f35e488

Browse files
committed
Release v0.0.47
## What's New ### Improvements & Fixes - **MCP Cache Fix** — Fixed MCP caching issues - **Tool UI Polish** — Improved expand button spacing in bash/edit tools - **Onboarding Cleanup** — Removed welcome dialog and streamlined sidebar
1 parent d9ba03a commit f35e488

9 files changed

Lines changed: 330 additions & 291 deletions

File tree

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.46",
3+
"version": "0.0.47",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {
@@ -68,6 +68,7 @@
6868
"@xterm/addon-web-links": "^0.12.0",
6969
"@xterm/addon-webgl": "^0.19.0",
7070
"ai": "^6.0.14",
71+
"async-mutex": "^0.5.0",
7172
"better-sqlite3": "^11.8.1",
7273
"chokidar": "^5.0.0",
7374
"class-variance-authority": "^0.7.1",

src/main/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -870,8 +870,8 @@ if (gotTheLock) {
870870
// This populates the cache so all future sessions can use filtered MCP servers
871871
setTimeout(async () => {
872872
try {
873-
const { warmupMcpCache } = await import("./lib/trpc/routers/claude")
874-
await warmupMcpCache()
873+
const { getAllMcpConfigHandler } = await import("./lib/trpc/routers/claude")
874+
await getAllMcpConfigHandler()
875875
} catch (error) {
876876
console.error("[App] MCP warmup failed:", error)
877877
}

src/main/lib/claude-config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Helpers for reading and writing ~/.claude.json configuration
33
*/
4+
import { Mutex } from "async-mutex"
45
import { eq } from "drizzle-orm"
56
import { existsSync, readFileSync, writeFileSync } from "fs"
67
import * as fs from "fs/promises"
@@ -9,6 +10,13 @@ import * as path from "path"
910
import { getDatabase } from "./db"
1011
import { chats, projects } from "./db/schema"
1112

13+
/**
14+
* Mutex for protecting read-modify-write operations on ~/.claude.json
15+
* This prevents race conditions when multiple concurrent operations
16+
* (e.g., token refreshes for different MCP servers) try to update the config.
17+
*/
18+
const configMutex = new Mutex()
19+
1220
export const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json")
1321

1422
export interface McpServerConfig {
@@ -76,6 +84,28 @@ export function writeClaudeConfigSync(config: ClaudeConfig): void {
7684
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8")
7785
}
7886

87+
/**
88+
* Execute a read-modify-write operation on ~/.claude.json atomically.
89+
* This is the ONLY safe way to update the config when concurrent writes are possible.
90+
*
91+
* Uses a mutex to ensure that only one read-modify-write cycle happens at a time,
92+
* preventing race conditions where concurrent token refreshes could overwrite
93+
* each other's updates.
94+
*
95+
* @param updater Function that receives current config and returns updated config
96+
* @returns The updated config
97+
*/
98+
export async function updateClaudeConfigAtomic(
99+
updater: (config: ClaudeConfig) => ClaudeConfig | Promise<ClaudeConfig>
100+
): Promise<ClaudeConfig> {
101+
return configMutex.runExclusive(async () => {
102+
const config = await readClaudeConfig()
103+
const updatedConfig = await updater(config)
104+
await writeClaudeConfig(updatedConfig)
105+
return updatedConfig
106+
})
107+
}
108+
79109
/**
80110
* Check if ~/.claude.json exists
81111
*/

src/main/lib/mcp-auth.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
getMcpServerConfig,
77
GLOBAL_MCP_PATH,
88
readClaudeConfig,
9+
updateClaudeConfigAtomic,
910
updateMcpServerConfig,
10-
writeClaudeConfig
1111
} from './claude-config';
1212
import { getClaudeShellEnvironment } from './claude/env';
1313
import { CraftOAuth, fetchOAuthMetadata, getMcpBaseUrl, type OAuthMetadata, type OAuthTokens } from './oauth';
@@ -389,42 +389,45 @@ export async function ensureMcpTokensFresh(
389389
return updatedServers;
390390
}
391391

392+
/**
393+
* Save OAuth tokens to ~/.claude.json atomically.
394+
* Uses a mutex to prevent race conditions when multiple concurrent
395+
* token refreshes try to update the config simultaneously.
396+
*/
392397
async function saveTokensToClaudeJson(
393398
serverName: string,
394399
projectPath: string,
395400
tokens: OAuthTokens,
396401
clientId?: string
397402
): Promise<void> {
398-
let config = await readClaudeConfig();
399-
400-
// Get existing server config to preserve existing headers and determine type
401-
const existingConfig = getMcpServerConfig(config, projectPath, serverName) || {};
402-
const serverUrl = existingConfig.url as string | undefined;
403-
404-
// Determine transport type from URL (SDK expects explicit type for HTTP servers)
405-
const serverType = serverUrl?.endsWith('/sse') ? 'sse' : 'http';
406-
407-
// Build headers with Authorization (preserve any existing headers)
408-
const existingHeaders = (existingConfig.headers as Record<string, string>) || {};
409-
const headers = {
410-
...existingHeaders,
411-
Authorization: `Bearer ${tokens.accessToken}`,
412-
};
413-
414-
config = updateMcpServerConfig(config, projectPath, serverName, {
415-
// SDK-required fields
416-
type: serverType,
417-
headers,
418-
// Internal tracking (for token refresh, status checking)
419-
_oauth: {
420-
accessToken: tokens.accessToken,
421-
refreshToken: tokens.refreshToken,
422-
clientId,
423-
expiresAt: tokens.expiresAt,
424-
},
403+
await updateClaudeConfigAtomic((config) => {
404+
// Get existing server config to preserve existing headers and determine type
405+
const existingConfig = getMcpServerConfig(config, projectPath, serverName) || {};
406+
const serverUrl = existingConfig.url as string | undefined;
407+
408+
// Determine transport type from URL (SDK expects explicit type for HTTP servers)
409+
const serverType = serverUrl?.endsWith('/sse') ? 'sse' : 'http';
410+
411+
// Build headers with Authorization (preserve any existing headers)
412+
const existingHeaders = (existingConfig.headers as Record<string, string>) || {};
413+
const headers = {
414+
...existingHeaders,
415+
Authorization: `Bearer ${tokens.accessToken}`,
416+
};
417+
418+
return updateMcpServerConfig(config, projectPath, serverName, {
419+
// SDK-required fields
420+
type: serverType,
421+
headers,
422+
// Internal tracking (for token refresh, status checking)
423+
_oauth: {
424+
accessToken: tokens.accessToken,
425+
refreshToken: tokens.refreshToken,
426+
clientId,
427+
expiresAt: tokens.expiresAt,
428+
},
429+
});
425430
});
426-
427-
await writeClaudeConfig(config);
428431
}
429432

430433
export function cancelAllPendingOAuth(): void {

0 commit comments

Comments
 (0)