Skip to content

Commit 9402595

Browse files
authored
MCP support
1 parent c90f4a7 commit 9402595

File tree

3 files changed

+119
-9
lines changed

3 files changed

+119
-9
lines changed

src/git.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ import { existsSync } from "fs";
1616

1717
const DEFAULT_CLONE_DIR = "/tmp/workspace";
1818

19+
// =============================================================================
20+
// Helpers
21+
// =============================================================================
22+
23+
/**
24+
* Checks if a branch exists on the remote using ls-remote.
25+
* This avoids masking auth/network errors as "branch not found".
26+
*/
27+
function branchExistsOnRemote(repoLocation: string, branchName: string): boolean {
28+
const output = execFileSync("git", ["ls-remote", "--heads", "origin", branchName], {
29+
cwd: repoLocation,
30+
encoding: "utf-8",
31+
stdio: ["pipe", "pipe", "pipe"],
32+
}).trim();
33+
return output.length > 0;
34+
}
35+
1936
// =============================================================================
2037
// Types
2138
// =============================================================================
@@ -93,20 +110,21 @@ export function cloneRepo(options: CloneRepoOptions): string {
93110
execFileSync("git", ["checkout", "-f", "HEAD"], { cwd: repoLocation });
94111

95112
if (branchName) {
96-
// Try to fetch and reset to the remote branch; create locally if it doesn't exist
97-
try {
113+
// Check if branch exists on remote before attempting fetch
114+
const remoteBranchExists = branchExistsOnRemote(repoLocation, branchName);
115+
if (remoteBranchExists) {
98116
execFileSync("git", ["fetch", "--depth", "2", "origin", branchName], { cwd: repoLocation, stdio: "pipe" });
99-
execFileSync("git", ["checkout", "-B", branchName, `origin/${branchName}`], { cwd: repoLocation });
117+
execFileSync("git", ["checkout", "-B", branchName, "FETCH_HEAD"], { cwd: repoLocation });
100118
console.log(`[Engine SDK] Checked out existing branch: ${branchName}`);
101-
} catch {
119+
} else {
102120
// Branch doesn't exist on remote — fetch default branch and create new branch from it
103121
execFileSync("git", ["fetch", "--depth", "2", "origin"], { cwd: repoLocation, stdio: "pipe" });
104122
execFileSync("git", ["checkout", "-B", branchName, "FETCH_HEAD"], { cwd: repoLocation });
105123
console.log(`[Engine SDK] Created new branch: ${branchName}`);
106124
}
107125
}
108126
} else {
109-
const authenticatedUrl = cloneUrl.replace("://", `://x-access-token:${gitToken}@`);
127+
const authHeader = `Authorization: basic ${Buffer.from(`x-access-token:${gitToken}`).toString("base64")}`;
110128

111129
// Try to clone the existing remote branch directly.
112130
// If the branch doesn't exist yet, fall back to a default clone + new branch.
@@ -116,7 +134,7 @@ export function cloneRepo(options: CloneRepoOptions): string {
116134
console.log(`[Engine SDK] Cloning ${repository} (branch: ${branchName}) to ${repoLocation}...`);
117135
execFileSync(
118136
"git",
119-
["clone", "-b", branchName, "--single-branch", "--depth", "2", authenticatedUrl, repoLocation],
137+
["-c", `http.extraHeader=${authHeader}`, "clone", "-b", branchName, "--single-branch", "--depth", "2", cloneUrl, repoLocation],
120138
{ stdio: "inherit" }
121139
);
122140
clonedExistingBranch = true;
@@ -127,7 +145,7 @@ export function cloneRepo(options: CloneRepoOptions): string {
127145

128146
if (!clonedExistingBranch) {
129147
console.log(`[Engine SDK] Cloning ${repository} to ${repoLocation}...`);
130-
execFileSync("git", ["clone", "--depth", "2", authenticatedUrl, repoLocation], {
148+
execFileSync("git", ["-c", `http.extraHeader=${authHeader}`, "clone", "--depth", "2", cloneUrl, repoLocation], {
131149
stdio: "inherit",
132150
});
133151

@@ -137,7 +155,7 @@ export function cloneRepo(options: CloneRepoOptions): string {
137155
}
138156
}
139157

140-
// Configure credentials and remote URL (replace embedded-token URL)
158+
// Configure credentials and remote URL
141159
configureGit();
142160
console.log(`[Engine SDK] Clone complete.`);
143161
}
@@ -167,7 +185,7 @@ export function commitAndPush(repoLocation: string, commitMessage: string): Comm
167185
execFileSync("git", ["commit", "-m", commitMessage], { cwd: repoLocation });
168186
}
169187

170-
execFileSync("git", ["push", "--force", "--set-upstream", "origin", "HEAD"], { cwd: repoLocation });
188+
execFileSync("git", ["push", "--set-upstream", "origin", "HEAD"], { cwd: repoLocation });
171189

172190
return {
173191
success: true,

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,11 @@ export type {
119119
export { cloneRepo, commitAndPush, finalizeChanges } from "./git.js";
120120

121121
export type { CloneRepoOptions, CommitAndPushResult } from "./git.js";
122+
123+
// =============================================================================
124+
// MCP Proxy Discovery
125+
// =============================================================================
126+
127+
export { discoverMCPServers, isMCPProxyAvailable } from "./mcp-proxy.js";
128+
129+
export type { MCPServerEntry, DiscoveredMCPServer } from "./mcp-proxy.js";

src/mcp-proxy.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
/**
6+
* MCP Proxy Discovery
7+
*
8+
* Discovers user-provided MCP servers from the platform's MCP proxy.
9+
* The proxy runs out-of-process (outside the firewall) and exposes
10+
* user MCP servers as HTTP MCP endpoints.
11+
*/
12+
13+
export interface MCPServerEntry {
14+
name: string;
15+
proxyEndpoint: string;
16+
}
17+
18+
export interface DiscoveredMCPServer {
19+
type: "http";
20+
url: string;
21+
}
22+
23+
/**
24+
* Checks whether the MCP proxy is available at the given URL.
25+
*/
26+
export async function isMCPProxyAvailable(proxyUrl: string): Promise<boolean> {
27+
try {
28+
const response = await fetch(`${proxyUrl}/health`, {
29+
signal: AbortSignal.timeout(5000),
30+
});
31+
return response.ok;
32+
} catch {
33+
return false;
34+
}
35+
}
36+
37+
/**
38+
* Discovers available MCP servers from the proxy and returns configs
39+
* ready to pass to the Copilot SDK's createSession mcpServers option.
40+
*
41+
* Returns an empty object if the proxy is unavailable or has no servers.
42+
*/
43+
export async function discoverMCPServers(
44+
proxyUrl: string,
45+
): Promise<Record<string, DiscoveredMCPServer>> {
46+
if (!(await isMCPProxyAvailable(proxyUrl))) {
47+
console.log(`[MCP] Proxy not available at ${proxyUrl}`);
48+
return {};
49+
}
50+
51+
try {
52+
const response = await fetch(`${proxyUrl}/mcp/servers`, {
53+
signal: AbortSignal.timeout(10000),
54+
});
55+
if (!response.ok) {
56+
console.warn(`[MCP] Failed to list servers: ${response.status}`);
57+
return {};
58+
}
59+
60+
const data = (await response.json()) as { servers: MCPServerEntry[] };
61+
const servers: Record<string, DiscoveredMCPServer> = {};
62+
63+
for (const server of data.servers) {
64+
const endpoint = server.proxyEndpoint.startsWith("http")
65+
? server.proxyEndpoint
66+
: `${proxyUrl}${server.proxyEndpoint}`;
67+
servers[server.name] = {
68+
type: "http",
69+
url: endpoint,
70+
};
71+
}
72+
73+
if (Object.keys(servers).length > 0) {
74+
console.log(`[MCP] Discovered ${Object.keys(servers).length} servers: ${Object.keys(servers).join(", ")}`);
75+
} else {
76+
console.log(`[MCP] Proxy available but no servers configured`);
77+
}
78+
79+
return servers;
80+
} catch (error) {
81+
console.warn(`[MCP] Failed to discover servers: ${error}`);
82+
return {};
83+
}
84+
}

0 commit comments

Comments
 (0)