fix: report OAuth metadata deployment errors

This commit is contained in:
2026-08-20 17:11:45 +08:00
parent e56ff6b1e1
commit ac4480bb09
9 changed files with 81 additions and 22 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ export function createProxyServer(upstream: {
listResourceTemplates: (params?: { cursor?: string }) => Promise<unknown>;
readResource: (params: { uri: string }) => Promise<unknown>;
}): Server {
const server = new Server({ name: "crafttable-mcp-stdio-bridge", version: "0.1.0" }, {
const server = new Server({ name: "crafttable-mcp-stdio-bridge", version: "0.1.1" }, {
capabilities: {
tools: {},
resources: {},
+1 -1
View File
@@ -13,7 +13,7 @@ type ConnectionFlags = { url?: string; clientId?: string; callbackPort?: string
const program = new Command()
.name("crafttable-mcp")
.description("OAuth client and stdio bridge for CraftTable MCP")
.version("0.1.0");
.version("0.1.1");
withConnection(program.command("login").description("Log in through the system browser"))
.option("--no-browser", "print the authorization URL instead of opening it")
+29 -6
View File
@@ -44,6 +44,7 @@ export async function loginRemote(
input: { browser: boolean; timeoutMs?: number; writeLine?: (value: string) => void } = { browser: true },
): Promise<{ alreadyAuthenticated: boolean; toolCount: number }> {
const writeLine = input.writeLine ?? ((value) => process.stderr.write(`${value}\n`));
await requireOAuthProtectedResourceMetadata(options.url);
let authorizationUrl: URL | undefined;
const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, async (url) => {
authorizationUrl = url;
@@ -60,7 +61,7 @@ export async function loginRemote(
});
const callback = new OAuthCallbackServer(options.callbackPort, (state) => provider.validateState(state));
await callback.listen();
const firstClient = new Client({ name: "crafttable-mcp-cli", version: "0.1.0" });
const firstClient = new Client({ name: "crafttable-mcp-cli", version: "0.1.1" });
const firstTransport = new StreamableHTTPClientTransport(options.url, { authProvider: provider });
try {
try {
@@ -109,7 +110,7 @@ async function connectWithTransport(
transport: StreamableHTTPClientTransport,
authentication: RemoteConnection["authentication"],
): Promise<RemoteConnection> {
const client = new Client({ name: "crafttable-mcp-cli", version: "0.1.0" });
const client = new Client({ name: "crafttable-mcp-cli", version: "0.1.1" });
try {
await client.connect(transport);
} catch (error) {
@@ -126,10 +127,7 @@ async function connectWithTransport(
}
async function discoverRevocationEndpoint(resource: URL, fetchFn: typeof fetch): Promise<URL> {
const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`, resource.origin);
const protectedResponse = await fetchFn(metadataUrl, { headers: { accept: "application/json" } });
if (!protectedResponse.ok) throw new Error(`OAuth protected-resource discovery returned HTTP ${protectedResponse.status}`);
const protectedMetadata = await protectedResponse.json() as { authorization_servers?: unknown };
const protectedMetadata = await requireOAuthProtectedResourceMetadata(resource, fetchFn);
const issuer = Array.isArray(protectedMetadata.authorization_servers)
? protectedMetadata.authorization_servers.find((value): value is string => typeof value === "string")
: undefined;
@@ -152,6 +150,31 @@ async function discoverRevocationEndpoint(resource: URL, fetchFn: typeof fetch):
throw new Error("OAuth authorization server does not advertise a revocation endpoint");
}
export async function requireOAuthProtectedResourceMetadata(
resource: URL,
fetchFn: typeof fetch = fetch,
): Promise<{ authorization_servers?: unknown }> {
const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`, resource.origin);
let response: Response;
try {
response = await fetchFn(metadataUrl, { headers: { accept: "application/json" } });
} catch (error) {
throw new Error(`Could not reach OAuth protected-resource metadata at ${metadataUrl}: ${errorMessage(error)}`);
}
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
if (!response.ok) {
throw new Error(`OAuth is not enabled for ${resource}; protected-resource metadata at ${metadataUrl} returned HTTP ${response.status}`);
}
if (!contentType.includes("application/json")) {
throw new Error(`OAuth is not enabled for ${resource}; protected-resource metadata at ${metadataUrl} returned ${contentType || "an unknown content type"} instead of JSON`);
}
try {
return await response.json() as { authorization_servers?: unknown };
} catch {
throw new Error(`OAuth protected-resource metadata at ${metadataUrl} is not valid JSON`);
}
}
async function revokeTokens(endpoint: URL, clientId: string, tokens: OAuthTokens, fetchFn: typeof fetch): Promise<void> {
const candidates = [tokens.refresh_token
? { token: tokens.refresh_token, hint: "refresh_token" }