Files
awesome-copilot/extensions/connector-namespaces/preview/server.mjs
T
Alex Yang [MSFT] 191309165e Add Azure Connector Namespaces canvas extension 🤖🤖🤖 (#2250)
* Add MCP Connectors (connector-namespaces) canvas extension

A Copilot CLI canvas extension for browsing and adding MCP connectors
from an Azure Connector Namespace into a Copilot session. Sign-in is
dependency-free (OAuth 2.0 auth-code + PKCE via the Azure CLI public
client, loopback redirect); network access is restricted to the public
Azure Resource Manager endpoint. MIT licensed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Connector Namespaces canvas extension

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden Connector Namespaces canvas extension

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sanitize connector icon brand colors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Connector Namespace playground actions

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Replace sandbox skill with native tool

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Clarify connector disconnect action

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Color disconnect action red

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Add connector extension plugin manifest

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Address connector canvas review feedback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Remove legacy connector canvas manifest

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Address remaining connector review feedback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Harden connector review fixes

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Harden connector convergence

Address the latest connector review batch across config persistence, executable trust, reauthentication, JSON-RPC transport, smoke safety, and accessibility.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Use native HTTP connector configs

Persist Connector Namespace MCP servers as direct HTTPS entries with API-key headers, remove the stdio unwrap proxy, and exercise the native Streamable HTTP path in smoke coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

* Secure persisted connector state

Create the Connector Namespace artifacts directory and saved gateway config with private permissions, and align the reduced-motion regression notes with the static fallback behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf

---------

Co-authored-by: Alex Yang <yangalex@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-16 10:29:48 +10:00

152 lines
5.8 KiB
JavaScript

// Standalone preview server for the connector-namespaces connector catalog.
//
// Renders every canvas state with no Copilot app, no ARM, and no real OAuth by
// importing the *pure* HTML builders from renderer.mjs and stubbing every
// /api/* endpoint the inline client script calls. Point any browser (or the
// agent-browser driver in shots.mjs) at it to see exactly what ships.
//
// Run: node extensions/connector-namespaces/preview/server.mjs
// Then open http://127.0.0.1:7331/ (catalog), /setup, /error.
//
// This process is NOT the JSON-RPC extension provider, so console.log here is
// fine and intentional — it is how you watch which stubbed endpoints get hit.
import { createServer } from "node:http";
import {
renderCatalogHtml,
renderSetupHtml,
renderErrorHtml,
} from "../renderer.mjs";
import * as fixtures from "./fixtures.mjs";
const HOST = "127.0.0.1";
const PORT = 7331;
const INSTANCE = "preview";
// Whatever /api/state should report next. The catalog route updates this from
// its query flags so a page load can force the banner / "added" tile on, and a
// real Connect click flips pendingRestart on via showRestartBanner().
let activeState = fixtures.stateEmpty;
function selectState(query) {
const restart = query.get("restart") === "1";
const installed = query.get("installed") === "1";
if (restart && installed) return fixtures.stateInstalledRestart;
if (installed) return { state: fixtures.stateInstalledRestart.state, pendingRestart: false };
if (restart) return { state: {}, pendingRestart: true };
return fixtures.stateEmpty;
}
function sendHtml(res, body) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(body);
}
function sendJson(res, obj, status = 200) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(obj));
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
if (!chunks.length) return {};
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
} catch {
return {};
}
}
const server = createServer(async (req, res) => {
const url = new URL(req.url, `http://${HOST}:${PORT}`);
const path = url.pathname;
const q = url.searchParams;
// Strip CR/LF/tab so a crafted request line can't forge extra log entries.
console.log(`${req.method} ${req.url}`.replace(/[\r\n\t]/g, " "));
// --- Page routes ---------------------------------------------------------
if (req.method === "GET" && (path === "/" || path === "/catalog")) {
activeState = selectState(q);
return sendHtml(
res,
renderCatalogHtml(INSTANCE, fixtures.catalog, {
filter: q.get("filter") || "",
category: q.get("category") || "all",
source: q.get("source") || "",
config: fixtures.config,
}),
);
}
if (req.method === "GET" && path === "/setup") {
return sendHtml(res, renderSetupHtml(fixtures.subscriptions));
}
if (req.method === "GET" && path === "/error") {
return sendHtml(res, renderErrorHtml(q.get("message") || "Something went wrong loading connectors."));
}
if (req.method === "GET" && path === "/fake-consent") {
return sendHtml(res, "<!doctype html><meta charset=utf-8><title>Consent</title><body style=\"font-family:sans-serif;padding:2rem\">Fake Microsoft consent page (preview). Close this tab.</body>");
}
// --- Stubbed API endpoints ----------------------------------------------
if (req.method === "GET" && path === "/api/state") {
return sendJson(res, activeState);
}
if (req.method === "GET" && path === "/api/gateways") {
return sendJson(res, { gateways: fixtures.gateways, hasMore: false });
}
if (req.method === "GET" && path === "/oauth-status") {
// Stay pending forever so the connecting spinner keeps animating for a
// screenshot. Flip to { done: true } if you want the full success flow.
return sendJson(res, { done: false });
}
if (req.method === "POST") {
await readBody(req);
switch (path) {
case "/api/select-gateway":
return sendJson(res, { ok: true });
case "/api/install":
return sendJson(res, fixtures.installNeedsConsent);
case "/api/finish-install":
activeState = { ...activeState, pendingRestart: true };
return sendJson(res, { ok: true });
case "/api/ack-restart":
activeState = { ...activeState, pendingRestart: false };
return sendJson(res, { ok: true });
case "/api/uninstall":
return sendJson(res, { ok: true });
case "/api/open-url":
// Preview no-op: do NOT actually launch a browser tab.
return sendJson(res, { ok: true });
case "/api/rollback-connection":
return sendJson(res, { ok: true });
default:
return sendJson(res, { error: `unstubbed POST ${path}` }, 404);
}
}
res.statusCode = 404;
res.end("not found");
});
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.error(`Port ${PORT} is already in use. Stop the other process or change PORT in server.mjs.`);
process.exit(1);
}
throw err;
});
server.listen(PORT, HOST, () => {
console.log(`canvas preview server: http://${HOST}:${PORT}/`);
console.log(" / catalog (empty state)");
console.log(" /?restart=1 catalog with restart banner visible");
console.log(" /?installed=1 catalog with one connector added");
console.log(" /setup namespace picker");
console.log(" /error error state");
console.log("Press Ctrl+C to stop.");
});