@civic/auth-mcp
Version:
Civic Auth integration for MCP servers
391 lines (382 loc) • 13.3 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/client/index.ts
var client_exports = {};
__export(client_exports, {
CLIAuthProvider: () => CLIAuthProvider,
CLIClient: () => CLIClient,
CivicAuthProvider: () => CivicAuthProvider,
DEFAULT_CALLBACK_PORT: () => DEFAULT_CALLBACK_PORT,
DEFAULT_MCP_ROUTE: () => DEFAULT_MCP_ROUTE,
DEFAULT_SCOPES: () => DEFAULT_SCOPES,
DEFAULT_WELLKNOWN_URL: () => DEFAULT_WELLKNOWN_URL,
InMemoryTokenPersistence: () => InMemoryTokenPersistence,
PUBLIC_CIVIC_CLIENT_ID: () => PUBLIC_CIVIC_CLIENT_ID,
RestartableStreamableHTTPClientTransport: () => RestartableStreamableHTTPClientTransport,
TokenAuthProvider: () => TokenAuthProvider
});
module.exports = __toCommonJS(client_exports);
// src/client/providers/persistence/InMemoryTokenPersistence.ts
var InMemoryTokenPersistence = class {
saveTokens(tokens) {
this.tokens = tokens;
}
loadTokens() {
return this.tokens;
}
clearTokens() {
this.tokens = void 0;
}
};
// src/client/providers/CivicAuthProvider.ts
var CivicAuthProvider = class {
constructor(options) {
this.clientSecret = options.clientSecret;
this.tokenPersistence = options.tokenPersistence ?? new InMemoryTokenPersistence();
}
saveTokens(tokens) {
return this.tokenPersistence.saveTokens(tokens);
}
/**
* Returns the stored tokens
*/
tokens() {
return this.tokenPersistence.loadTokens();
}
/**
* Clears the stored tokens
*/
clearTokens() {
return this.tokenPersistence.clearTokens();
}
};
// src/client/providers/CLIAuthProvider.ts
var import_node_child_process = require("child_process");
var import_node_crypto = __toESM(require("crypto"), 1);
var import_node_http = __toESM(require("http"), 1);
var import_node_url = __toESM(require("url"), 1);
var import_node_util = require("util");
var import_escape_html = __toESM(require("escape-html"), 1);
// src/constants.ts
var DEFAULT_WELLKNOWN_URL = "https://auth.civic.com/oauth/.well-known/openid-configuration";
var DEFAULT_SCOPES = ["openid", "profile", "email", "offline_access"];
var DEFAULT_CALLBACK_PORT = 8080;
var DEFAULT_MCP_ROUTE = "/mcp";
var PUBLIC_CIVIC_CLIENT_ID = "12220cf4-1a9a-4964-8eb7-7c6d7d049f34";
// src/client/providers/CLIAuthProvider.ts
var CLIAuthProvider = class extends CivicAuthProvider {
constructor(options) {
super(options);
this.clientId = options.clientId;
this.scope = options.scope ?? DEFAULT_SCOPES.join(" ");
this.callbackPort = options.callbackPort ?? DEFAULT_CALLBACK_PORT;
this.enablePortFallback = options.enablePortFallback ?? true;
this.authTimeoutMs = options.authTimeoutMs ?? 5 * 60 * 1e3;
this.successHtml = options.successHtml ?? '<html lang="en"><body><h1>Authorization Successful</h1><p>You can now close this window.</p></body></html>';
this.errorHtml = options.errorHtml ?? '<html lang="en"><body><h1>Authorization Failed</h1><p>{{error}}</p></body></html>';
}
clientInformation() {
const info = {
client_id: this.clientId
};
if (this.clientSecret) {
info.client_secret = this.clientSecret;
}
return info;
}
get clientMetadata() {
return {
redirect_uris: [this.getCallbackUrl(this.callbackPort)],
client_name: this.clientId,
scope: this.scope
};
}
codeVerifier() {
if (!this.storedCodeVerifier) {
this.storedCodeVerifier = import_node_crypto.default.randomBytes(32).toString("base64url");
}
return this.storedCodeVerifier;
}
async redirectToAuthorization(authorizationUrl) {
if (this.callbackServer) {
throw new Error("Authorization flow already in progress. Please wait for it to complete.");
}
console.log(`Opening authorization URL in browser: ${authorizationUrl.href}`);
const actualPort = await this.startCallbackServer();
let urlToOpen = authorizationUrl.href;
if (actualPort) {
this.callbackPort = actualPort;
const authUrlObj = new URL(authorizationUrl);
authUrlObj.searchParams.set("redirect_uri", this.getCallbackUrl(actualPort));
urlToOpen = authUrlObj.href;
}
await this.openInBrowser(urlToOpen);
console.log("Please complete the authorization in your browser.");
}
/**
* Registers the transport with the auth provider so that we can call finishAuth when the code is received.
* @param transport
*/
registerTransport(transport) {
this.transport = transport;
}
get redirectUrl() {
return new URL(this.getCallbackUrl(this.callbackPort));
}
saveCodeVerifier(codeVerifier) {
this.storedCodeVerifier = codeVerifier;
}
getCallbackUrl(port) {
return `http://localhost:${port}/callback`;
}
/**
* Listen on Port Promise
* @param server
* @param port
* @private port that is being listened on.
*/
listenOnPort(server, port) {
return new Promise((resolve, reject) => {
const onError = (err) => {
server.off("listening", onListening);
reject(err);
};
const onListening = () => {
server.off("error", onError);
const address = server.address();
resolve(address.port);
};
server.once("error", onError);
server.once("listening", onListening);
server.listen(port, "localhost");
});
}
/**
* Starts a local HTTP server to handle the OAuth callback with port fallback support
* @returns The actual port number if different from the configured port, undefined otherwise
*/
async startCallbackServer() {
this.authorizationCodePromise = new Promise((resolveCode, rejectCode) => {
this.authorizationCodeResolve = resolveCode;
this.authorizationCodeReject = rejectCode;
});
this.callbackServer = import_node_http.default.createServer((req, res) => {
try {
if (!req.url) {
res.writeHead(400);
res.end("Bad Request");
return;
}
const parsedUrl = import_node_url.default.parse(req.url, true);
if (parsedUrl.pathname === "/callback") {
const code = parsedUrl.query.code;
const error = parsedUrl.query.error;
if (error) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(this.errorHtml.replace("{{error}}", (0, import_escape_html.default)(error)));
this.authorizationCodeReject?.(new Error(`OAuth error: ${error}`));
} else if (code) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(this.successHtml);
if (this.transport) {
this.transport.finishAuth(code).then(() => this.authorizationCodeResolve?.(code)).catch((error2) => {
console.error("Error in finishAuth:", error2);
this.authorizationCodeReject?.(error2);
});
} else {
this.authorizationCodeReject?.(new Error("No transport registered"));
}
} else {
res.writeHead(400);
res.end("Missing authorization code");
}
} else {
res.writeHead(404);
res.end("Not Found");
}
} finally {
this.cleanup();
}
});
let actualPort;
try {
actualPort = await this.listenOnPort(this.callbackServer, this.callbackPort);
} catch (err) {
if (err.code === "EADDRINUSE" && this.enablePortFallback) {
console.warn(`Port ${this.callbackPort} in use. Trying a random port...`);
actualPort = await this.listenOnPort(this.callbackServer, 0);
} else {
throw err;
}
}
this.serverTimeout = setTimeout(() => {
console.warn(`OAuth callback server timeout reached after ${this.authTimeoutMs / 1e3}s. Closing server.`);
this.cleanup();
}, this.authTimeoutMs);
return actualPort !== this.callbackPort ? actualPort : void 0;
}
/**
* Resets the instance to its post-initialization state
* Stops any active server, clears timeouts
*/
cleanup() {
if (this.callbackServer) {
this.callbackServer.close();
this.callbackServer = void 0;
}
if (this.serverTimeout) {
clearTimeout(this.serverTimeout);
this.serverTimeout = void 0;
}
}
/**
* Waits for the authorization code from the callback
*/
async waitForAuthorizationCode() {
if (!this.authorizationCodePromise) {
throw new Error("Authorization flow not started");
}
return this.authorizationCodePromise;
}
async openInBrowser(url2) {
const execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
try {
switch (process.platform) {
case "darwin":
await execFileAsync("open", [url2]);
break;
case "win32":
await execFileAsync("cmd", ["/c", "start", url2]);
break;
default:
await execFileAsync("xdg-open", [url2]);
}
} catch (error) {
console.error("Failed to open browser:", error);
console.log("Please open this URL manually:", url2);
}
}
};
// src/client/providers/TokenAuthProvider.ts
var TokenAuthProvider = class extends CivicAuthProvider {
/**
* Create a new TokenAuthProvider
* @param tokenOrOptions - Either a token string or full options object
*/
constructor(tokenOrOptions) {
const options = typeof tokenOrOptions === "string" ? { tokens: { access_token: tokenOrOptions, token_type: "Bearer" } } : tokenOrOptions;
super(options);
this.tokenPersistence.saveTokens(options.tokens);
}
get redirectUrl() {
return "";
}
get clientMetadata() {
return {
redirect_uris: []
};
}
clientInformation() {
return {
client_id: "token-client"
};
}
redirectToAuthorization(_authorizationUrl) {
}
saveCodeVerifier(_codeVerifier) {
}
codeVerifier() {
return "";
}
};
// src/client/transport/RestartableStreamableHTTPClientTransport.ts
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
var RestartableStreamableHTTPClientTransport = class extends import_streamableHttp.StreamableHTTPClientTransport {
constructor(url2, opts) {
super(url2, opts);
this._cliAuthProvider = opts.authProvider;
this._cliAuthProvider.registerTransport(this);
}
get authProvider() {
return this._cliAuthProvider;
}
/**
* Extends the start method to properly handle reconnection.
* If the transport has already been started, it will disconnect first,
* then start again to establish a fresh connection.
*/
async start() {
try {
await super.start();
} catch (_error) {
}
}
async close() {
}
};
// src/client/CLIClient.ts
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
var CLIClient = class extends import_client.Client {
/**
* Connect to MCP server with automatic authentication handling
* If the first connection fails due to auth, it will wait for the OAuth flow
* to complete and then retry the connection
*/
async connect(transport) {
try {
await super.connect(transport);
} catch (error) {
if (error instanceof Error) {
if (error.message === "Unauthorized") {
console.log("Authorization required, waiting for user to complete OAuth flow...");
const authProvider = transport.authProvider;
await authProvider.waitForAuthorizationCode();
console.log("Authorization completed.");
return await super.connect(transport);
}
}
throw error;
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CLIAuthProvider,
CLIClient,
CivicAuthProvider,
DEFAULT_CALLBACK_PORT,
DEFAULT_MCP_ROUTE,
DEFAULT_SCOPES,
DEFAULT_WELLKNOWN_URL,
InMemoryTokenPersistence,
PUBLIC_CIVIC_CLIENT_ID,
RestartableStreamableHTTPClientTransport,
TokenAuthProvider
});
//# sourceMappingURL=index.cjs.map