Skip to content

Commit 948653e

Browse files
Dan Shapiroclaude
andcommitted
feat: inject freshell MCP server into SDK sessions
Freshclaude (agent-chat) sessions now get the freshell MCP tool automatically, matching what terminal-mode Claude already gets via --mcp-config. The MCP server config includes FRESHELL_URL and FRESHELL_TOKEN env vars so the subprocess can connect back. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 53d6da1 commit 948653e

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

server/sdk-bridge.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type Query as SdkQuery,
1414
} from '@anthropic-ai/claude-agent-sdk'
1515
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
16+
import { buildMcpServerCommandArgs } from './mcp/config-writer.js'
1617
import { formatModelDisplayName } from '../shared/format-model-name.js'
1718
import { logger } from './logger.js'
1819
import type {
@@ -96,6 +97,16 @@ export class SdkBridge extends EventEmitter {
9697
includePartialMessages: true,
9798
abortController,
9899
env: cleanEnv,
100+
mcpServers: {
101+
freshell: {
102+
command: 'node',
103+
args: buildMcpServerCommandArgs(),
104+
env: {
105+
FRESHELL_URL: process.env.FRESHELL_URL || `http://localhost:${Number(process.env.PORT || 3001)}`,
106+
FRESHELL_TOKEN: process.env.AUTH_TOKEN || '',
107+
},
108+
},
109+
},
99110
stderr: (data: string) => {
100111
log.warn({ sessionId, data: data.trimEnd() }, 'SDK subprocess stderr')
101112
},

test/unit/server/sdk-bridge.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,91 @@ describe('SdkBridge', () => {
888888
})
889889
})
890890

891+
describe('mcpServers injection', () => {
892+
it('passes freshell MCP server config to SDK query', async () => {
893+
await bridge.createSession({ cwd: '/tmp' })
894+
expect(mockQueryOptions?.mcpServers).toBeDefined()
895+
expect(mockQueryOptions.mcpServers.freshell).toBeDefined()
896+
expect(mockQueryOptions.mcpServers.freshell.command).toBe('node')
897+
expect(Array.isArray(mockQueryOptions.mcpServers.freshell.args)).toBe(true)
898+
expect(mockQueryOptions.mcpServers.freshell.args.length).toBeGreaterThan(0)
899+
})
900+
901+
it('includes FRESHELL_URL and FRESHELL_TOKEN in MCP server env', async () => {
902+
await bridge.createSession({ cwd: '/tmp' })
903+
const env = mockQueryOptions?.mcpServers?.freshell?.env
904+
expect(env).toBeDefined()
905+
expect(env.FRESHELL_URL).toBeDefined()
906+
expect(typeof env.FRESHELL_URL).toBe('string')
907+
expect(typeof env.FRESHELL_TOKEN).toBe('string')
908+
})
909+
910+
it('derives FRESHELL_URL from PORT env var', async () => {
911+
const origPort = process.env.PORT
912+
const origUrl = process.env.FRESHELL_URL
913+
process.env.PORT = '4455'
914+
delete process.env.FRESHELL_URL
915+
try {
916+
await bridge.createSession({ cwd: '/tmp' })
917+
expect(mockQueryOptions.mcpServers.freshell.env.FRESHELL_URL).toBe('http://localhost:4455')
918+
} finally {
919+
if (origPort !== undefined) process.env.PORT = origPort
920+
else delete process.env.PORT
921+
if (origUrl !== undefined) process.env.FRESHELL_URL = origUrl
922+
else delete process.env.FRESHELL_URL
923+
}
924+
})
925+
926+
it('uses FRESHELL_URL env var when set', async () => {
927+
const origUrl = process.env.FRESHELL_URL
928+
process.env.FRESHELL_URL = 'http://custom:9999'
929+
try {
930+
await bridge.createSession({ cwd: '/tmp' })
931+
expect(mockQueryOptions.mcpServers.freshell.env.FRESHELL_URL).toBe('http://custom:9999')
932+
} finally {
933+
if (origUrl !== undefined) process.env.FRESHELL_URL = origUrl
934+
else delete process.env.FRESHELL_URL
935+
}
936+
})
937+
938+
it('uses AUTH_TOKEN env var as FRESHELL_TOKEN', async () => {
939+
const origToken = process.env.AUTH_TOKEN
940+
process.env.AUTH_TOKEN = 'test-secret-token'
941+
try {
942+
await bridge.createSession({ cwd: '/tmp' })
943+
expect(mockQueryOptions.mcpServers.freshell.env.FRESHELL_TOKEN).toBe('test-secret-token')
944+
} finally {
945+
if (origToken !== undefined) process.env.AUTH_TOKEN = origToken
946+
else delete process.env.AUTH_TOKEN
947+
}
948+
})
949+
950+
it('defaults FRESHELL_URL to localhost:3001 when no env vars set', async () => {
951+
const origPort = process.env.PORT
952+
const origUrl = process.env.FRESHELL_URL
953+
delete process.env.PORT
954+
delete process.env.FRESHELL_URL
955+
try {
956+
await bridge.createSession({ cwd: '/tmp' })
957+
expect(mockQueryOptions.mcpServers.freshell.env.FRESHELL_URL).toBe('http://localhost:3001')
958+
} finally {
959+
if (origPort !== undefined) process.env.PORT = origPort
960+
if (origUrl !== undefined) process.env.FRESHELL_URL = origUrl
961+
}
962+
})
963+
964+
it('defaults FRESHELL_TOKEN to empty string when AUTH_TOKEN unset', async () => {
965+
const origToken = process.env.AUTH_TOKEN
966+
delete process.env.AUTH_TOKEN
967+
try {
968+
await bridge.createSession({ cwd: '/tmp' })
969+
expect(mockQueryOptions.mcpServers.freshell.env.FRESHELL_TOKEN).toBe('')
970+
} finally {
971+
if (origToken !== undefined) process.env.AUTH_TOKEN = origToken
972+
}
973+
})
974+
})
975+
891976
describe('setModel', () => {
892977
it('returns false for nonexistent session', () => {
893978
expect(bridge.setModel('nonexistent', 'claude-sonnet-4-5-20250929')).toBe(false)

0 commit comments

Comments
 (0)