@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/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&hellip;</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
- this.server.tool(name, description, zodSchemaShape, async (args, extra) => {
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
- logger.info("[MCPICloudflareAgent] 🔑 Credential auth required:", {
908
- tool: toolName,
909
- provider,
910
- scopes,
911
- consentUrl: consentUrl.substring(0, 80) + "...",
912
- });
913
- // ✅ FIX: Store error instead of throwing to ensure proper formatting
914
- pendingAuthError = new DelegationRequiredError(toolName, scopes, consentUrl, undefined, resumeToken);
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
- // For credential/password providers, build consent URL pointing to credential form
987
- logger.info("[MCPICloudflareAgent] 🔑 Credential auth required (from OAuthRequiredError):", {
988
- tool: oauthError.toolName,
989
- provider: oauthError.provider,
990
- scopes: oauthError.requiredScopes,
991
- });
992
- authUrl = await this.buildConsentUrlForCredentials(oauthError.toolName, oauthError.provider || "credentials", oauthError.requiredScopes, sessionId, mappedEnv);
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
- // Generate resume token using runtime's internal method
999
- // The runtime will generate it when DelegationRequiredError is thrown
1000
- const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1001
- // ✅ FIX: Store error instead of throwing to ensure proper formatting
1002
- pendingAuthError = new DelegationRequiredError(oauthError.toolName, oauthError.requiredScopes, authUrl, undefined, // interceptedCall
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
- if (!pendingAuthError) {
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 with isError: true (MCP standard for user-visible errors)
1098
- // This format is what Claude Desktop and MCP Inspector display to users
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: true,
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");