@kya-os/mcp-i-cloudflare 1.7.76 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +5 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +31 -5
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts +13 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +601 -30
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +35 -0
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/proof-generator.d.ts.map +1 -1
- package/dist/proof-generator.js +12 -10
- package/dist/proof-generator.js.map +1 -1
- package/dist/providers/crypto.d.ts +1 -1
- package/dist/providers/crypto.d.ts.map +1 -1
- package/dist/providers/crypto.js +5 -1
- package/dist/providers/crypto.js.map +1 -1
- package/dist/services/consent.service.d.ts +18 -0
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +151 -36
- package/dist/services/consent.service.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -9
package/dist/agent.js
CHANGED
|
@@ -65,6 +65,7 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
65
65
|
_autoDetectedOrigin; // Auto-detected server origin from request URL
|
|
66
66
|
_clientMessagesConfig; // Client-specific delegation messages
|
|
67
67
|
_clientProvidedSessionId; // Deprecated: Use KV mapping instead to avoid race conditions
|
|
68
|
+
_consentUIResourceUri; // MCP Apps consent UI resource URI (set if ext-apps available)
|
|
68
69
|
constructor(state, env, providerRegistry = defaultProviderRegistry) {
|
|
69
70
|
// Call super() with just state and env (agents@0.2.21+ only accepts 2 parameters)
|
|
70
71
|
// The config is no longer passed to the constructor - it's set via the server property
|
|
@@ -89,6 +90,11 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
89
90
|
mappedEnv._durableObjectState = state;
|
|
90
91
|
// Load runtime configuration from subclass
|
|
91
92
|
const runtimeConfig = this.getRuntimeConfigInternal(mappedEnv);
|
|
93
|
+
// Allow MCPI_INLINE_CONSENT env var to override the config flag
|
|
94
|
+
if (mappedEnv.MCPI_INLINE_CONSENT === "true" ||
|
|
95
|
+
mappedEnv.MCPI_INLINE_CONSENT === "1") {
|
|
96
|
+
runtimeConfig.inlineConsent = true;
|
|
97
|
+
}
|
|
92
98
|
// Store environment for logging checks
|
|
93
99
|
this._environment = runtimeConfig.environment || "production";
|
|
94
100
|
// Create tool protection service if configured
|
|
@@ -166,6 +172,16 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
166
172
|
const clientInfo = params?.clientInfo;
|
|
167
173
|
const protocolVersion = params?.protocolVersion;
|
|
168
174
|
const capabilities = params?.capabilities;
|
|
175
|
+
// Log raw capabilities from client for debugging ext-apps support
|
|
176
|
+
console.error("[MCPICloudflareAgent] Client initialize capabilities:", {
|
|
177
|
+
clientName: clientInfo?.name,
|
|
178
|
+
protocolVersion,
|
|
179
|
+
hasCapabilities: !!capabilities,
|
|
180
|
+
capabilityKeys: capabilities ? Object.keys(capabilities) : [],
|
|
181
|
+
hasExtensions: !!capabilities?.extensions,
|
|
182
|
+
extensionKeys: capabilities?.extensions ? Object.keys(capabilities.extensions) : [],
|
|
183
|
+
rawCapabilities: JSON.stringify(capabilities)?.substring(0, 500),
|
|
184
|
+
});
|
|
169
185
|
// Store client info for later retrieval (always, not just in dev)
|
|
170
186
|
if (clientInfo || protocolVersion) {
|
|
171
187
|
await this.ctx.storage.put("mcpClientInfo", {
|
|
@@ -500,6 +516,9 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
500
516
|
if (!this.server) {
|
|
501
517
|
throw new Error("Server not initialized. This should not happen - server is initialized in constructor.");
|
|
502
518
|
}
|
|
519
|
+
// Register MCP Apps consent UI resource BEFORE tools
|
|
520
|
+
// so that registerToolWithProof can add _meta.ui to tool definitions
|
|
521
|
+
await this.registerConsentUIResource();
|
|
503
522
|
// Register tools (implemented by subclasses)
|
|
504
523
|
await this.registerTools();
|
|
505
524
|
}
|
|
@@ -508,6 +527,492 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
508
527
|
throw error;
|
|
509
528
|
}
|
|
510
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Register the MCP Apps consent UI resource.
|
|
532
|
+
*
|
|
533
|
+
* Uses the existing <mcp-consent> Lit components bundled into a
|
|
534
|
+
* self-contained HTML string. The resource is served via standard
|
|
535
|
+
* MCP resources/read -- hosts that support MCP Apps will render it
|
|
536
|
+
* in a sandboxed iframe when a tool requires delegation.
|
|
537
|
+
*
|
|
538
|
+
* Gracefully no-ops when @modelcontextprotocol/ext-apps or
|
|
539
|
+
* @kya-os/consent/mcp-app are not available.
|
|
540
|
+
*/
|
|
541
|
+
async registerConsentUIResource() {
|
|
542
|
+
try {
|
|
543
|
+
// Dynamic imports to handle optional peer dependencies
|
|
544
|
+
const [extApps, consentMcpApp, extAppsConstants] = await Promise.all([
|
|
545
|
+
import("@modelcontextprotocol/ext-apps/server").catch(() => null),
|
|
546
|
+
import("@kya-os/consent/mcp-app").catch(() => null),
|
|
547
|
+
import("@kya-os/mcp-i-core/runtime/ext-apps-constants").catch(() => null),
|
|
548
|
+
]);
|
|
549
|
+
if (!extApps || !consentMcpApp || !extAppsConstants) {
|
|
550
|
+
logger.info("[MCPICloudflareAgent] MCP Apps consent UI not available (missing optional dependencies)");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const { registerAppResource, RESOURCE_MIME_TYPE } = extApps;
|
|
554
|
+
// consentMcpApp imported for future full Lit-based UI; inline HTML used for now
|
|
555
|
+
const { CONSENT_UI_RESOURCE_URI } = extAppsConstants;
|
|
556
|
+
// Determine server URL for CSP connect-src
|
|
557
|
+
const serverUrl = this.env.MCP_SERVER_URL ??
|
|
558
|
+
this._autoDetectedOrigin;
|
|
559
|
+
const connectDomains = ["*.kya.vouched.id"];
|
|
560
|
+
if (serverUrl) {
|
|
561
|
+
try {
|
|
562
|
+
connectDomains.push(new URL(serverUrl).hostname);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Invalid URL, skip
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
registerAppResource(this.server, "MCPI Consent", CONSENT_UI_RESOURCE_URI, {
|
|
569
|
+
description: "Authorization consent form for protected tools",
|
|
570
|
+
_meta: {
|
|
571
|
+
ui: {
|
|
572
|
+
csp: {
|
|
573
|
+
connectDomains,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
}, async () => {
|
|
578
|
+
// Use minimal inline handshake HTML while debugging LIT bundle issues.
|
|
579
|
+
// The full CONSENT_MCP_APP_HTML (628KB) loads but doesn't render —
|
|
580
|
+
// likely a CSP or sandboxed iframe restriction on the LIT bundle.
|
|
581
|
+
// This minimal version proves the data pipeline works end-to-end.
|
|
582
|
+
const html = `<!DOCTYPE html>
|
|
583
|
+
<html lang="en">
|
|
584
|
+
<head>
|
|
585
|
+
<meta charset="utf-8">
|
|
586
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
587
|
+
<meta name="color-scheme" content="light dark">
|
|
588
|
+
<title>MCPI Consent</title>
|
|
589
|
+
<style>
|
|
590
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
591
|
+
body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; background: transparent; }
|
|
592
|
+
.card { border-radius: 12px; padding: 20px; border: 1px solid rgba(128,128,128,0.2); }
|
|
593
|
+
h2 { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
594
|
+
p { font-size: 13px; opacity: 0.7; line-height: 1.5; margin-bottom: 6px; }
|
|
595
|
+
.scopes { margin: 10px 0; }
|
|
596
|
+
.scope { display: inline-block; padding: 3px 10px; border-radius: 10px; font-size: 12px; font-weight: 500; margin: 2px 4px 2px 0; background: rgba(46,125,50,0.1); color: #2e7d32; }
|
|
597
|
+
.btn { display: inline-block; padding: 8px 20px; border-radius: 8px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; margin-right: 8px; margin-top: 10px; }
|
|
598
|
+
.btn-approve { background: #2563eb; color: #fff; }
|
|
599
|
+
.btn-approve:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
600
|
+
.btn-deny { background: transparent; border: 1px solid rgba(128,128,128,0.3); }
|
|
601
|
+
.info { font-size: 11px; opacity: 0.5; margin-top: 12px; }
|
|
602
|
+
.fallback { font-size: 12px; margin-top: 8px; }
|
|
603
|
+
.fallback a { color: #2563eb; }
|
|
604
|
+
#loading { font-size: 13px; opacity: 0.6; }
|
|
605
|
+
#consent { display: none; }
|
|
606
|
+
#approved { display: none; font-size: 14px; color: #16a34a; font-weight: 500; }
|
|
607
|
+
.cred-fields { display: none; margin: 12px 0; }
|
|
608
|
+
.cred-fields label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px; opacity: 0.8; }
|
|
609
|
+
.cred-fields input[type="text"],
|
|
610
|
+
.cred-fields input[type="password"] { width: 100%; padding: 8px 10px; border-radius: 6px; border: 1px solid rgba(128,128,128,0.3); font-size: 14px; margin-bottom: 10px; background: transparent; color: inherit; }
|
|
611
|
+
.cred-fields input:focus { outline: none; border-color: #2563eb; }
|
|
612
|
+
.pw-wrap { position: relative; }
|
|
613
|
+
.pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; cursor: pointer; font-size: 13px; opacity: 0.5; color: inherit; }
|
|
614
|
+
.terms-row { display: flex; align-items: flex-start; gap: 8px; margin: 10px 0 4px; }
|
|
615
|
+
.terms-row input[type="checkbox"] { margin-top: 2px; }
|
|
616
|
+
.terms-row label { font-size: 12px; opacity: 0.7; cursor: pointer; }
|
|
617
|
+
.auth-error { display: none; font-size: 13px; color: #dc2626; margin: 8px 0; padding: 8px 10px; border-radius: 6px; background: rgba(220,38,38,0.08); }
|
|
618
|
+
</style>
|
|
619
|
+
</head>
|
|
620
|
+
<body>
|
|
621
|
+
<div id="loading">Waiting for consent data…</div>
|
|
622
|
+
<div id="consent" class="card">
|
|
623
|
+
<h2>Authorization Required</h2>
|
|
624
|
+
<p id="tool-desc"></p>
|
|
625
|
+
<div id="scopes-container" class="scopes"></div>
|
|
626
|
+
<div id="cred-fields" class="cred-fields">
|
|
627
|
+
<label for="cred-username">Username / Email</label>
|
|
628
|
+
<input type="text" id="cred-username" autocomplete="username" placeholder="Enter your username or email">
|
|
629
|
+
<label for="cred-password">Password</label>
|
|
630
|
+
<div class="pw-wrap">
|
|
631
|
+
<input type="password" id="cred-password" autocomplete="current-password" placeholder="Enter your password">
|
|
632
|
+
<button type="button" class="pw-toggle" id="pw-toggle" aria-label="Toggle password visibility">Show</button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div id="auth-error" class="auth-error"></div>
|
|
636
|
+
<div id="terms-row" class="terms-row" style="display:none;">
|
|
637
|
+
<input type="checkbox" id="terms-check">
|
|
638
|
+
<label for="terms-check">I agree to authorize this tool and accept the terms of service</label>
|
|
639
|
+
</div>
|
|
640
|
+
<p id="agent-info" class="info"></p>
|
|
641
|
+
<div>
|
|
642
|
+
<button class="btn btn-approve" id="approve-btn">Approve</button>
|
|
643
|
+
<button class="btn btn-deny" id="deny-btn">Deny</button>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="fallback" id="fallback"></div>
|
|
646
|
+
</div>
|
|
647
|
+
<div id="approved"></div>
|
|
648
|
+
<script>
|
|
649
|
+
(function() {
|
|
650
|
+
var send = function(msg) { window.parent.postMessage(msg, "*"); };
|
|
651
|
+
var consentData = null;
|
|
652
|
+
var authMode = "consent-only";
|
|
653
|
+
|
|
654
|
+
function resizeNotify() {
|
|
655
|
+
send({ jsonrpc: "2.0", method: "ui/notifications/size-changed", params: {
|
|
656
|
+
width: document.documentElement.scrollWidth,
|
|
657
|
+
height: document.documentElement.scrollHeight
|
|
658
|
+
}});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function showConsent(data) {
|
|
662
|
+
consentData = data;
|
|
663
|
+
authMode = data.authMode || "consent-only";
|
|
664
|
+
document.getElementById("loading").style.display = "none";
|
|
665
|
+
document.getElementById("consent").style.display = "block";
|
|
666
|
+
|
|
667
|
+
if (authMode === "credentials") {
|
|
668
|
+
document.getElementById("tool-desc").textContent =
|
|
669
|
+
"Tool \u201c" + (data.tool || "unknown") + "\u201d requires authentication.";
|
|
670
|
+
document.getElementById("cred-fields").style.display = "block";
|
|
671
|
+
document.getElementById("terms-row").style.display = "flex";
|
|
672
|
+
document.getElementById("approve-btn").textContent = "Sign In & Authorize";
|
|
673
|
+
document.getElementById("approve-btn").disabled = true;
|
|
674
|
+
document.getElementById("terms-check").addEventListener("change", function() {
|
|
675
|
+
document.getElementById("approve-btn").disabled = !this.checked;
|
|
676
|
+
});
|
|
677
|
+
} else {
|
|
678
|
+
document.getElementById("tool-desc").textContent =
|
|
679
|
+
"Tool \u201c" + (data.tool || "unknown") + "\u201d requires your approval.";
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
var scopesEl = document.getElementById("scopes-container");
|
|
683
|
+
var scopes = data.scopes || [];
|
|
684
|
+
scopesEl.textContent = "";
|
|
685
|
+
scopes.forEach(function(s) {
|
|
686
|
+
var span = document.createElement("span");
|
|
687
|
+
span.className = "scope";
|
|
688
|
+
span.textContent = s;
|
|
689
|
+
scopesEl.appendChild(span);
|
|
690
|
+
});
|
|
691
|
+
document.getElementById("agent-info").textContent =
|
|
692
|
+
"Agent: " + (data.agentName || data.agentDid || "unknown");
|
|
693
|
+
if (data.consentUrl) {
|
|
694
|
+
var fallbackEl = document.getElementById("fallback");
|
|
695
|
+
fallbackEl.textContent = "";
|
|
696
|
+
var a = document.createElement("a");
|
|
697
|
+
a.href = data.consentUrl;
|
|
698
|
+
a.target = "_blank";
|
|
699
|
+
a.rel = "noopener";
|
|
700
|
+
a.textContent = "Open in browser";
|
|
701
|
+
fallbackEl.appendChild(a);
|
|
702
|
+
}
|
|
703
|
+
resizeNotify();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Password visibility toggle
|
|
707
|
+
document.getElementById("pw-toggle").addEventListener("click", function() {
|
|
708
|
+
var pwInput = document.getElementById("cred-password");
|
|
709
|
+
if (pwInput.type === "password") {
|
|
710
|
+
pwInput.type = "text";
|
|
711
|
+
this.textContent = "Hide";
|
|
712
|
+
} else {
|
|
713
|
+
pwInput.type = "password";
|
|
714
|
+
this.textContent = "Show";
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
var pendingRequests = {};
|
|
719
|
+
var nextId = 100;
|
|
720
|
+
|
|
721
|
+
function handleApprovalResponse(result, error) {
|
|
722
|
+
var statusEl = document.getElementById("approved");
|
|
723
|
+
if (error) {
|
|
724
|
+
// Show error inline for credential mode (allow retry)
|
|
725
|
+
if (authMode === "credentials") {
|
|
726
|
+
var errEl = document.getElementById("auth-error");
|
|
727
|
+
errEl.textContent = error.message || JSON.stringify(error);
|
|
728
|
+
errEl.style.display = "block";
|
|
729
|
+
document.getElementById("consent").style.display = "block";
|
|
730
|
+
statusEl.style.display = "none";
|
|
731
|
+
document.getElementById("approve-btn").disabled = !document.getElementById("terms-check").checked;
|
|
732
|
+
resizeNotify();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
statusEl.textContent = "Approval error: " + (error.message || JSON.stringify(error));
|
|
736
|
+
statusEl.style.color = "#dc2626";
|
|
737
|
+
} else {
|
|
738
|
+
try {
|
|
739
|
+
var text = result && result.content && result.content[0] && result.content[0].text;
|
|
740
|
+
var respData = text ? JSON.parse(text) : {};
|
|
741
|
+
if (respData.success) {
|
|
742
|
+
statusEl.textContent = "Authorization granted. You can now retry the tool.";
|
|
743
|
+
statusEl.style.color = "#22c55e";
|
|
744
|
+
} else {
|
|
745
|
+
// For credential auth failures, show error inline and allow retry
|
|
746
|
+
if (authMode === "credentials" && (respData.error_code === "auth_failed" || respData.error_code === "validation_error")) {
|
|
747
|
+
var errEl = document.getElementById("auth-error");
|
|
748
|
+
errEl.textContent = respData.error || "Authentication failed. Please check your credentials.";
|
|
749
|
+
errEl.style.display = "block";
|
|
750
|
+
document.getElementById("consent").style.display = "block";
|
|
751
|
+
statusEl.style.display = "none";
|
|
752
|
+
document.getElementById("approve-btn").disabled = !document.getElementById("terms-check").checked;
|
|
753
|
+
resizeNotify();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
statusEl.textContent = "Approval error: " + (respData.error || respData.error_code || "Unknown");
|
|
757
|
+
statusEl.style.color = "#f59e0b";
|
|
758
|
+
}
|
|
759
|
+
} catch(e) {
|
|
760
|
+
statusEl.textContent = "Unexpected response";
|
|
761
|
+
statusEl.style.color = "#f59e0b";
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
resizeNotify();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
document.getElementById("approve-btn").addEventListener("click", function() {
|
|
768
|
+
if (!consentData) return;
|
|
769
|
+
// Hide any previous error
|
|
770
|
+
document.getElementById("auth-error").style.display = "none";
|
|
771
|
+
var statusEl = document.getElementById("approved");
|
|
772
|
+
document.getElementById("consent").style.display = "none";
|
|
773
|
+
statusEl.style.display = "block";
|
|
774
|
+
statusEl.textContent = authMode === "credentials" ? "Authenticating..." : "Sending approval...";
|
|
775
|
+
statusEl.style.color = "";
|
|
776
|
+
// Use tools/call via postMessage (proxied through host to MCP server)
|
|
777
|
+
// instead of fetch() which is blocked in the sandboxed iframe.
|
|
778
|
+
var reqId = nextId++;
|
|
779
|
+
pendingRequests[reqId] = handleApprovalResponse;
|
|
780
|
+
|
|
781
|
+
if (authMode === "credentials") {
|
|
782
|
+
var username = document.getElementById("cred-username").value;
|
|
783
|
+
var password = document.getElementById("cred-password").value;
|
|
784
|
+
if (!username || !password) {
|
|
785
|
+
statusEl.style.display = "none";
|
|
786
|
+
document.getElementById("consent").style.display = "block";
|
|
787
|
+
var errEl = document.getElementById("auth-error");
|
|
788
|
+
errEl.textContent = "Please enter both username and password.";
|
|
789
|
+
errEl.style.display = "block";
|
|
790
|
+
resizeNotify();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
send({
|
|
794
|
+
jsonrpc: "2.0", id: reqId, method: "tools/call",
|
|
795
|
+
params: {
|
|
796
|
+
name: "_mcpi_credential_auth",
|
|
797
|
+
arguments: {
|
|
798
|
+
tool: consentData.tool,
|
|
799
|
+
scopes: consentData.scopes,
|
|
800
|
+
session_id: consentData.sessionId,
|
|
801
|
+
agent_did: consentData.agentDid,
|
|
802
|
+
project_id: consentData.projectId,
|
|
803
|
+
resume_token: consentData.resumeToken,
|
|
804
|
+
username: username,
|
|
805
|
+
password: password,
|
|
806
|
+
provider: consentData.provider || "credentials"
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
} else {
|
|
811
|
+
send({
|
|
812
|
+
jsonrpc: "2.0", id: reqId, method: "tools/call",
|
|
813
|
+
params: {
|
|
814
|
+
name: "_mcpi_approve_consent",
|
|
815
|
+
arguments: {
|
|
816
|
+
tool: consentData.tool,
|
|
817
|
+
scopes: consentData.scopes,
|
|
818
|
+
session_id: consentData.sessionId,
|
|
819
|
+
agent_did: consentData.agentDid,
|
|
820
|
+
project_id: consentData.projectId,
|
|
821
|
+
resume_token: consentData.resumeToken
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
resizeNotify();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
document.getElementById("deny-btn").addEventListener("click", function() {
|
|
830
|
+
document.getElementById("consent").style.display = "none";
|
|
831
|
+
document.getElementById("approved").style.display = "block";
|
|
832
|
+
document.getElementById("approved").textContent = "Authorization denied.";
|
|
833
|
+
document.getElementById("approved").style.color = "#dc2626";
|
|
834
|
+
resizeNotify();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
window.addEventListener("message", function(e) {
|
|
838
|
+
if (e.source !== window.parent) return;
|
|
839
|
+
var msg = e.data;
|
|
840
|
+
if (!msg || msg.jsonrpc !== "2.0") return;
|
|
841
|
+
|
|
842
|
+
if (msg.id === 0 && msg.result) {
|
|
843
|
+
var ctx = msg.result.hostContext || {};
|
|
844
|
+
if (ctx.theme === "dark") document.documentElement.style.colorScheme = "dark";
|
|
845
|
+
send({ jsonrpc: "2.0", method: "ui/notifications/initialized" });
|
|
846
|
+
resizeNotify();
|
|
847
|
+
}
|
|
848
|
+
// Handle responses to our pending tools/call requests
|
|
849
|
+
if (msg.id != null && pendingRequests[msg.id]) {
|
|
850
|
+
pendingRequests[msg.id](msg.result, msg.error);
|
|
851
|
+
delete pendingRequests[msg.id];
|
|
852
|
+
}
|
|
853
|
+
if (msg.method === "ui/notifications/tool-result" && msg.params) {
|
|
854
|
+
var sc = msg.params.structuredContent;
|
|
855
|
+
if (sc && sc.type === "consent_required") showConsent(sc);
|
|
856
|
+
}
|
|
857
|
+
if (msg.method === "ui/notifications/tool-input" && msg.params) {
|
|
858
|
+
var args = msg.params.arguments;
|
|
859
|
+
if (args && args.type === "consent_required") showConsent(args);
|
|
860
|
+
}
|
|
861
|
+
if (msg.method === "ping" && msg.id != null) {
|
|
862
|
+
send({ jsonrpc: "2.0", id: msg.id, result: {} });
|
|
863
|
+
}
|
|
864
|
+
if (msg.method === "ui/resource-teardown" && msg.id != null) {
|
|
865
|
+
send({ jsonrpc: "2.0", id: msg.id, result: {} });
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
send({
|
|
870
|
+
jsonrpc: "2.0", id: 0, method: "ui/initialize",
|
|
871
|
+
params: {
|
|
872
|
+
appInfo: { name: "MCPI Consent", version: "1.0.0" },
|
|
873
|
+
appCapabilities: {},
|
|
874
|
+
protocolVersion: "2026-01-26"
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
})();
|
|
878
|
+
</script>
|
|
879
|
+
</body>
|
|
880
|
+
</html>`;
|
|
881
|
+
return {
|
|
882
|
+
contents: [
|
|
883
|
+
{
|
|
884
|
+
uri: CONSENT_UI_RESOURCE_URI,
|
|
885
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
886
|
+
text: html,
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
};
|
|
890
|
+
});
|
|
891
|
+
// Store the resource URI so registerToolWithProof can add _meta.ui to tool definitions
|
|
892
|
+
this._consentUIResourceUri = CONSENT_UI_RESOURCE_URI;
|
|
893
|
+
// Signal to MCPIRuntimeBase that ext-apps is available.
|
|
894
|
+
// The base class's isExtAppsAvailable() uses synchronous require() which
|
|
895
|
+
// fails for ESM-only packages in CJS/Workers contexts. This cached flag
|
|
896
|
+
// ensures buildConsentUIResult() works correctly at tool-call time.
|
|
897
|
+
this.mcpiRuntime?.setExtAppsAvailable(true);
|
|
898
|
+
// Register internal tool for iframe-based consent approval.
|
|
899
|
+
// The sandboxed MCP Apps iframe cannot call fetch() directly, so it
|
|
900
|
+
// uses tools/call (proxied via postMessage through the host) to invoke
|
|
901
|
+
// this tool which handles the /consent/approve logic server-side.
|
|
902
|
+
this.server.tool("_mcpi_approve_consent", "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process approval actions. Only the embedded consent form should invoke this.", {
|
|
903
|
+
tool: z.string(),
|
|
904
|
+
scopes: z.array(z.string()),
|
|
905
|
+
session_id: z.string(),
|
|
906
|
+
agent_did: z.string(),
|
|
907
|
+
project_id: z.string(),
|
|
908
|
+
resume_token: z.string(),
|
|
909
|
+
}, async (args) => {
|
|
910
|
+
const envPrefix = this.getEnvPrefix();
|
|
911
|
+
const mappedEnv = envPrefix
|
|
912
|
+
? mapPrefixedEnv(this.env, envPrefix)
|
|
913
|
+
: this.env;
|
|
914
|
+
const serverUrl = mappedEnv.MCP_SERVER_URL ??
|
|
915
|
+
this._autoDetectedOrigin;
|
|
916
|
+
if (!serverUrl) {
|
|
917
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "No server URL configured" }) }] };
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const resp = await fetch(`${serverUrl}/consent/approve`, {
|
|
921
|
+
method: "POST",
|
|
922
|
+
headers: { "Content-Type": "application/json" },
|
|
923
|
+
body: JSON.stringify({
|
|
924
|
+
tool: args.tool,
|
|
925
|
+
scopes: args.scopes,
|
|
926
|
+
session_id: args.session_id,
|
|
927
|
+
agent_did: args.agent_did,
|
|
928
|
+
project_id: args.project_id,
|
|
929
|
+
resume_token: args.resume_token,
|
|
930
|
+
termsAccepted: true,
|
|
931
|
+
approved: true,
|
|
932
|
+
}),
|
|
933
|
+
});
|
|
934
|
+
if (!resp.ok) {
|
|
935
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Server error: ${resp.status}` }) }] };
|
|
936
|
+
}
|
|
937
|
+
const data = await resp.json();
|
|
938
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
939
|
+
}
|
|
940
|
+
catch (err) {
|
|
941
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
942
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }] };
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
// Register internal tool for iframe-based credential authentication.
|
|
946
|
+
// Same sandbox workaround as _mcpi_approve_consent: the iframe calls
|
|
947
|
+
// tools/call via postMessage, which the host proxies to this tool.
|
|
948
|
+
// This tool does credential auth + delegation in a single step.
|
|
949
|
+
this.server.tool("_mcpi_credential_auth", "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process credential authentication. Only the embedded consent form should invoke this.", {
|
|
950
|
+
tool: z.string(),
|
|
951
|
+
scopes: z.array(z.string()),
|
|
952
|
+
session_id: z.string(),
|
|
953
|
+
agent_did: z.string(),
|
|
954
|
+
project_id: z.string(),
|
|
955
|
+
resume_token: z.string(),
|
|
956
|
+
username: z.string(),
|
|
957
|
+
password: z.string(),
|
|
958
|
+
provider: z.string(),
|
|
959
|
+
}, async (args) => {
|
|
960
|
+
const envPrefix = this.getEnvPrefix();
|
|
961
|
+
const mappedEnv = envPrefix
|
|
962
|
+
? mapPrefixedEnv(this.env, envPrefix)
|
|
963
|
+
: this.env;
|
|
964
|
+
const serverUrl = mappedEnv.MCP_SERVER_URL ??
|
|
965
|
+
this._autoDetectedOrigin;
|
|
966
|
+
if (!serverUrl) {
|
|
967
|
+
return {
|
|
968
|
+
content: [
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: JSON.stringify({
|
|
972
|
+
success: false,
|
|
973
|
+
error: "No server URL configured",
|
|
974
|
+
}),
|
|
975
|
+
},
|
|
976
|
+
],
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
const resp = await fetch(`${serverUrl}/consent/approve`, {
|
|
981
|
+
method: "POST",
|
|
982
|
+
headers: { "Content-Type": "application/json" },
|
|
983
|
+
body: JSON.stringify({
|
|
984
|
+
tool: args.tool,
|
|
985
|
+
scopes: args.scopes,
|
|
986
|
+
session_id: args.session_id,
|
|
987
|
+
agent_did: args.agent_did,
|
|
988
|
+
project_id: args.project_id,
|
|
989
|
+
provider_type: "password",
|
|
990
|
+
provider: args.provider,
|
|
991
|
+
username: args.username,
|
|
992
|
+
password: args.password,
|
|
993
|
+
inline_mode: true,
|
|
994
|
+
termsAccepted: true,
|
|
995
|
+
approved: true,
|
|
996
|
+
}),
|
|
997
|
+
});
|
|
998
|
+
if (!resp.ok) {
|
|
999
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Server error: ${resp.status}` }) }] };
|
|
1000
|
+
}
|
|
1001
|
+
const data = await resp.json();
|
|
1002
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
1006
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }] };
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
logger.info("[MCPICloudflareAgent] Registered MCP Apps consent UI resource");
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
// ext-apps or consent/mcp-app not available - graceful no-op
|
|
1013
|
+
logger.info("[MCPICloudflareAgent] MCP Apps consent UI not available (ext-apps or consent/mcp-app not installed)");
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
511
1016
|
/**
|
|
512
1017
|
* Register a tool with automatic session ID extraction and proof generation
|
|
513
1018
|
*
|
|
@@ -602,14 +1107,32 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
602
1107
|
zodSchemaShape = schema;
|
|
603
1108
|
}
|
|
604
1109
|
}
|
|
605
|
-
|
|
1110
|
+
// Use registerTool (not deprecated server.tool) to support _meta.ui for MCP Apps
|
|
1111
|
+
const toolCallback = async (args, extra) => {
|
|
606
1112
|
// ✅ Automatically extract sessionId from ToolExtraArguments
|
|
607
1113
|
// This ensures delegation tokens stored with the original session ID
|
|
608
1114
|
// can be retrieved on subsequent tool calls
|
|
609
|
-
// Note: extra is typed as 'any' to match MCP SDK's ToolExtraArguments type
|
|
610
1115
|
const sessionId = extra?.sessionId;
|
|
611
1116
|
return this.executeToolWithProof(name, args, handler, sessionId);
|
|
612
|
-
}
|
|
1117
|
+
};
|
|
1118
|
+
// Build tool config with optional MCP Apps UI metadata
|
|
1119
|
+
// When ext-apps consent UI is registered, include _meta.ui.resourceUri
|
|
1120
|
+
// so clients know this tool can render inline consent UI
|
|
1121
|
+
const toolConfig = {
|
|
1122
|
+
description,
|
|
1123
|
+
inputSchema: zodSchemaShape,
|
|
1124
|
+
};
|
|
1125
|
+
if (this._consentUIResourceUri) {
|
|
1126
|
+
// Include BOTH new nested format AND legacy flat format for maximum client compatibility
|
|
1127
|
+
// registerAppTool() from ext-apps SDK normalizes both; we replicate that here
|
|
1128
|
+
// New format: _meta.ui.resourceUri (preferred)
|
|
1129
|
+
// Legacy format: _meta["ui/resourceUri"] (deprecated but some clients may check this)
|
|
1130
|
+
toolConfig._meta = {
|
|
1131
|
+
ui: { resourceUri: this._consentUIResourceUri },
|
|
1132
|
+
"ui/resourceUri": this._consentUIResourceUri,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
this.server.registerTool(name, toolConfig, toolCallback);
|
|
613
1136
|
}
|
|
614
1137
|
catch (error) {
|
|
615
1138
|
logger.error(`[MCPICloudflareAgent] Failed to register tool "${name}":`, error);
|
|
@@ -857,6 +1380,7 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
857
1380
|
createdAt: timestamp,
|
|
858
1381
|
expiresAt: timestamp + 30 * 60 * 1000, // 30 minutes
|
|
859
1382
|
serverOrigin: serverUrl, // Use MCP_SERVER_URL for consent URL building
|
|
1383
|
+
projectId: mappedEnv.AGENTSHIELD_PROJECT_ID || "", // For inline consent UI approve POST
|
|
860
1384
|
delegationToken, // ✅ Include token in session if found
|
|
861
1385
|
delegationId, // ✅ Include delegationId for proof submission attribution
|
|
862
1386
|
userDid, // ✅ FIX: Include userDid in session for ToolExecutionContext propagation
|
|
@@ -883,6 +1407,8 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
883
1407
|
// ✅ SECURITY FIX: Store original auth error to re-throw if URL building fails
|
|
884
1408
|
// This prevents unauthorized tool execution when URL building fails
|
|
885
1409
|
let originalAuthError = null;
|
|
1410
|
+
// ✅ MCP Apps inline UI result for credential auth (returns UI instead of throwing)
|
|
1411
|
+
let pendingInlineResult = null;
|
|
886
1412
|
try {
|
|
887
1413
|
toolContext = await this.buildToolContext(toolName, userDid, sessionId, delegationToken, mappedEnv);
|
|
888
1414
|
}
|
|
@@ -901,17 +1427,31 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
901
1427
|
const scopes = credError.toolProtection?.requiredScopes || [
|
|
902
1428
|
`${toolName}:execute`,
|
|
903
1429
|
];
|
|
904
|
-
// Build consent page URL with credentials mode
|
|
905
|
-
const consentUrl = await this.buildConsentUrlForCredentials(toolName, provider, scopes, sessionId, mappedEnv);
|
|
906
1430
|
const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1431
|
+
// Try MCP Apps inline credential UI first (preferred path)
|
|
1432
|
+
// Note: clientCapabilities not available in this context — pass undefined.
|
|
1433
|
+
// buildConsentUIResult gates on isExtAppsAvailable(), not client caps.
|
|
1434
|
+
const inlineResult = this.mcpiRuntime.buildConsentUIResult(toolName, scopes, session, resumeToken, session.projectId, session.serverOrigin, undefined, "credentials", provider);
|
|
1435
|
+
if (inlineResult) {
|
|
1436
|
+
logger.info("[MCPICloudflareAgent] 🔑 Credential auth required (inline UI):", {
|
|
1437
|
+
tool: toolName,
|
|
1438
|
+
provider,
|
|
1439
|
+
scopes,
|
|
1440
|
+
});
|
|
1441
|
+
pendingInlineResult = inlineResult;
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
// ext-apps not available — fall back to external consent URL
|
|
1445
|
+
const consentUrl = await this.buildConsentUrlForCredentials(toolName, provider, scopes, sessionId, mappedEnv);
|
|
1446
|
+
logger.info("[MCPICloudflareAgent] 🔑 Credential auth required:", {
|
|
1447
|
+
tool: toolName,
|
|
1448
|
+
provider,
|
|
1449
|
+
scopes,
|
|
1450
|
+
consentUrl: consentUrl.substring(0, 80) + "...",
|
|
1451
|
+
});
|
|
1452
|
+
// ✅ FIX: Store error instead of throwing to ensure proper formatting
|
|
1453
|
+
pendingAuthError = new DelegationRequiredError(toolName, scopes, consentUrl, undefined, resumeToken);
|
|
1454
|
+
}
|
|
915
1455
|
}
|
|
916
1456
|
catch (credentialError) {
|
|
917
1457
|
// If URL building fails, log the error but continue
|
|
@@ -982,25 +1522,37 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
982
1522
|
isCredentialProvider,
|
|
983
1523
|
});
|
|
984
1524
|
let authUrl;
|
|
1525
|
+
const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
985
1526
|
if (isCredentialProvider) {
|
|
986
|
-
//
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1527
|
+
// Try MCP Apps inline credential UI first
|
|
1528
|
+
const inlineResult = this.mcpiRuntime?.buildConsentUIResult(oauthError.toolName, oauthError.requiredScopes, session, resumeToken, session.projectId, session.serverOrigin, undefined, "credentials", oauthError.provider || "credentials");
|
|
1529
|
+
if (inlineResult) {
|
|
1530
|
+
logger.info("[MCPICloudflareAgent] 🔑 Credential auth required (inline UI from OAuthRequiredError):", {
|
|
1531
|
+
tool: oauthError.toolName,
|
|
1532
|
+
provider: oauthError.provider,
|
|
1533
|
+
scopes: oauthError.requiredScopes,
|
|
1534
|
+
});
|
|
1535
|
+
pendingInlineResult = inlineResult;
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
// ext-apps not available — fall back to external consent URL
|
|
1539
|
+
logger.info("[MCPICloudflareAgent] 🔑 Credential auth required (from OAuthRequiredError):", {
|
|
1540
|
+
tool: oauthError.toolName,
|
|
1541
|
+
provider: oauthError.provider,
|
|
1542
|
+
scopes: oauthError.requiredScopes,
|
|
1543
|
+
});
|
|
1544
|
+
authUrl = await this.buildConsentUrlForCredentials(oauthError.toolName, oauthError.provider || "credentials", oauthError.requiredScopes, sessionId, mappedEnv);
|
|
1545
|
+
}
|
|
993
1546
|
}
|
|
994
1547
|
else {
|
|
995
1548
|
// For OAuth providers (github, google, etc.), build OAuth URL
|
|
996
1549
|
authUrl = await this.buildOAuthUrlForError(oauthError, sessionId, mappedEnv);
|
|
997
1550
|
}
|
|
998
|
-
//
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
resumeToken);
|
|
1551
|
+
// Only create DelegationRequiredError if we didn't get an inline result
|
|
1552
|
+
if (!pendingInlineResult) {
|
|
1553
|
+
pendingAuthError = new DelegationRequiredError(oauthError.toolName, oauthError.requiredScopes, authUrl, undefined, // interceptedCall
|
|
1554
|
+
resumeToken);
|
|
1555
|
+
}
|
|
1004
1556
|
}
|
|
1005
1557
|
catch (oauthError) {
|
|
1006
1558
|
// If URL building fails, log the error
|
|
@@ -1015,7 +1567,8 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
1015
1567
|
// ✅ SECURITY FIX: If we had an auth error but URL building failed,
|
|
1016
1568
|
// we MUST re-throw the original error to prevent unauthorized tool execution.
|
|
1017
1569
|
// Only log and continue for non-auth errors (backward compatibility).
|
|
1018
|
-
|
|
1570
|
+
// Note: pendingInlineResult means auth is handled via inline UI — skip re-throw.
|
|
1571
|
+
if (!pendingAuthError && !pendingInlineResult) {
|
|
1019
1572
|
if (originalAuthError) {
|
|
1020
1573
|
// Auth was required but URL building failed - MUST block execution
|
|
1021
1574
|
logger.error("[MCPICloudflareAgent] SECURITY: Auth required but URL building failed, blocking tool execution:", { originalError: originalAuthError.message, buildError: error });
|
|
@@ -1050,6 +1603,10 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
1050
1603
|
}
|
|
1051
1604
|
};
|
|
1052
1605
|
// Execute tool with automatic proof generation
|
|
1606
|
+
// If inline credential UI was prepared, return it directly (no error throw needed)
|
|
1607
|
+
if (pendingInlineResult) {
|
|
1608
|
+
return pendingInlineResult;
|
|
1609
|
+
}
|
|
1053
1610
|
// Wrap in try-catch to handle DelegationRequiredError and format for MCP clients
|
|
1054
1611
|
try {
|
|
1055
1612
|
// ✅ FIX: Throw pendingAuthError INSIDE the try block
|
|
@@ -1094,8 +1651,8 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
1094
1651
|
scopes,
|
|
1095
1652
|
clientId,
|
|
1096
1653
|
});
|
|
1097
|
-
// Return tool result
|
|
1098
|
-
//
|
|
1654
|
+
// Return tool result WITHOUT isError so the LLM renders the markdown link directly.
|
|
1655
|
+
// With isError: true, Claude paraphrases the content instead of rendering it.
|
|
1099
1656
|
return {
|
|
1100
1657
|
content: [
|
|
1101
1658
|
{
|
|
@@ -1103,7 +1660,7 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
1103
1660
|
text: displayMessage,
|
|
1104
1661
|
},
|
|
1105
1662
|
],
|
|
1106
|
-
isError:
|
|
1663
|
+
isError: false,
|
|
1107
1664
|
};
|
|
1108
1665
|
}
|
|
1109
1666
|
// Handle OAuthRequiredError - use formatOAuth() to preserve provider name
|
|
@@ -1927,6 +2484,20 @@ export class MCPICloudflareAgent extends McpAgent {
|
|
|
1927
2484
|
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
1928
2485
|
}
|
|
1929
2486
|
}
|
|
2487
|
+
// Handle internal client capabilities request
|
|
2488
|
+
// Returns stored MCP client capabilities from the initialize message
|
|
2489
|
+
if (url.pathname === "/_internal/client-capabilities" && request.method === "GET") {
|
|
2490
|
+
try {
|
|
2491
|
+
const mcpClientInfo = await this.getMcpClientInfo();
|
|
2492
|
+
return new Response(JSON.stringify({
|
|
2493
|
+
success: true,
|
|
2494
|
+
capabilities: mcpClientInfo?.capabilities || null,
|
|
2495
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
return new Response(JSON.stringify({ success: false, capabilities: null }), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
1930
2501
|
// Handle internal cache-clear request
|
|
1931
2502
|
if (url.pathname === "/_do/cache-clear" && request.method === "POST") {
|
|
1932
2503
|
logger.info("[MCPICloudflareAgent] Handling internal cache-clear request");
|