* Normalize a single MCP server config from an external source. We do not run * the full `normalizeServerConfig` from `mcp/config.ts` because it has side * effects (env expansion) and Claude's shape is already a subset of ours. * Only stdio / http / sse entries with the required fields survive.
(raw: unknown)
| 122 | * Only stdio / http / sse entries with the required fields survive. |
| 123 | */ |
| 124 | function normalizeExternalServer(raw: unknown): McpServerConfig | undefined { |
| 125 | if (!isPlainObject(raw)) return undefined; |
| 126 | const type = typeof raw.type === "string" ? raw.type : "stdio"; |
| 127 | |
| 128 | if (type === "stdio") { |
| 129 | if (typeof raw.command !== "string" || !raw.command.trim()) |
| 130 | return undefined; |
| 131 | const config: McpStdioServerConfig = { |
| 132 | type: "stdio", |
| 133 | command: raw.command, |
| 134 | }; |
| 135 | if (Array.isArray(raw.args)) { |
| 136 | const args = raw.args.filter((a): a is string => typeof a === "string"); |
| 137 | if (args.length > 0) config.args = args; |
| 138 | } |
| 139 | if (isPlainObject(raw.env)) { |
| 140 | const env: Record<string, string> = {}; |
| 141 | for (const [k, v] of Object.entries(raw.env)) { |
| 142 | if (typeof v === "string") env[k] = v; |
| 143 | } |
| 144 | if (Object.keys(env).length > 0) config.env = env; |
| 145 | } |
| 146 | return config; |
| 147 | } |
| 148 | |
| 149 | if (type === "http" || type === "sse") { |
| 150 | if (typeof raw.url !== "string" || !raw.url.trim()) return undefined; |
| 151 | const config: McpHttpServerConfig | McpSseServerConfig = { |
| 152 | type, |
| 153 | url: raw.url, |
| 154 | }; |
| 155 | if (isPlainObject(raw.headers)) { |
| 156 | const headers: Record<string, string> = {}; |
| 157 | for (const [k, v] of Object.entries(raw.headers)) { |
| 158 | if (typeof v === "string") headers[k] = v; |
| 159 | } |
| 160 | if (Object.keys(headers).length > 0) config.headers = headers; |
| 161 | } |
| 162 | return config; |
| 163 | } |
| 164 | |
| 165 | // `streamable-http` is Claude Code's preferred name for the modern HTTP |
| 166 | // transport; OrbCode aliases it as "http". |
| 167 | if (type === "streamable-http" || type === "streamable_http") { |
| 168 | if (typeof raw.url !== "string" || !raw.url.trim()) return undefined; |
| 169 | const config: McpHttpServerConfig = { type: "http", url: raw.url }; |
| 170 | if (isPlainObject(raw.headers)) { |
| 171 | const headers: Record<string, string> = {}; |
| 172 | for (const [k, v] of Object.entries(raw.headers)) { |
| 173 | if (typeof v === "string") headers[k] = v; |
| 174 | } |
| 175 | if (Object.keys(headers).length > 0) config.headers = headers; |
| 176 | } |
| 177 | return config; |
| 178 | } |
| 179 | |
| 180 | return undefined; |
| 181 | } |
no test coverage detected