kloudy 0.4.7 → 0.4.8

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/README.md CHANGED
@@ -312,3 +312,5 @@ Official MCP Registry name: `ai.kloudy/kloudy`. Remote endpoint: https://kloudy.
312
312
 
313
313
  The remote MCP adapter returns supported card metadata and a plain-text HTTPS chooser link together. Hosts control rendering and link launch. Call show_pending_chooser_link to display the last offer/selection again in the same MCP process. Website sign-in verifies private account projects; the public template locator is never an account token or authorization. A presentation goal may be retained in the current MCP process and is kept out of the URL. Automatic authenticated IDE return is not claimed. If the process resets, use the website's existing project recovery path. No models, installation or spending are activated by these links.
314
314
 
315
+
316
+ Website return attachment (0.4.8): show_visual_setup_choices retains the visible HTTPS chooser link and supported card metadata. With a goal, it also offers a waiting-connection link and matching code. On the website, use the existing verified account, 2FA and authorized project; confirm the matching waiting IDE. receive_website_selection then receives the exact encrypted inert manifest on that same MCP connection. No extension, website cookie in the IDE, copied account credential, new engine grant, installation, execution or spending is required. The public locator is not authorization. Delivery keys stay in this process; restarting it requires a fresh pairing, while the website restores the existing project's saved state. An unread receipt cannot be overwritten by a second selection. Hosts control native card display; no universal native-chat rendering claim is made.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kloudy",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Ask for Kloudy: CLI, MCP and SDK clients for the same scoped engine session.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/remote.mjs CHANGED
@@ -8,6 +8,7 @@ import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,
8
8
  import {REMOTE} from './editor-install.mjs';
9
9
  import {autoupdateEnabled} from './autoupdate.mjs';
10
10
  import {chooserResponse,pendingChooserResponse,pendingChooserTool} from './chooser-link.mjs';
11
+ import {WebsiteSelectionReturn,websiteReturnTool} from './website-return.mjs';
11
12
  const transports=new WeakMap();
12
13
  // This proxy can relay Apps rendering, but cannot service sampling, roots or elicitation.
13
14
  export function forwardedCapabilities(value={}){
@@ -30,6 +31,7 @@ export async function askRemote(question,options){
30
31
  export async function serveRemote(options={}){
31
32
  const connect=options.connect??connectRemote,enabled=autoupdateEnabled(options.autoupdate??process.env.KLOUDY_MCP_AUTOUPDATE);
32
33
  let remote=await connect({...options,capabilities:{}}),ready=Promise.resolve(),closed=false,reconnecting,pendingChooser,requestGoal;
34
+ const attachment=options.websiteReturn??(options.connect?null:new WebsiteSelectionReturn());
33
35
  const upstream=remote.getServerCapabilities(),capabilities={};
34
36
  if(upstream.tools)capabilities.tools=enabled&&upstream.tools.listChanged?{listChanged:true}:{};
35
37
  if(upstream.resources)capabilities.resources=enabled&&upstream.resources.listChanged?{listChanged:true}:{};
@@ -58,9 +60,10 @@ export async function serveRemote(options={}){
58
60
  if(capabilities.tools){
59
61
  server.setRequestHandler(ListToolsRequestSchema,async req=>{
60
62
  const result=await invoke('listTools',req.params);
61
- return {...result,tools:[...result.tools.map(tool=>tool.name==='show_visual_setup_choices'?{...tool,inputSchema:{...tool.inputSchema,properties:{...tool.inputSchema.properties,goal:{type:'string',minLength:1,maxLength:1000,description:'Presentation request retained in this MCP process; never sent as a credential or URL.'}}}}:tool),...(result.tools.some(t=>t.name===pendingChooserTool.name)?[]:[pendingChooserTool])]};
63
+ return {...result,tools:[...result.tools.map(tool=>tool.name==='show_visual_setup_choices'?{...tool,inputSchema:{...tool.inputSchema,properties:{...tool.inputSchema.properties,goal:{type:'string',minLength:1,maxLength:1000,description:'Presentation request retained in this MCP process; never sent as a credential or URL.'}}}}:tool),...(result.tools.some(t=>t.name===pendingChooserTool.name)?[]:[pendingChooserTool]),websiteReturnTool]};
62
64
  });
63
65
  server.setRequestHandler(CallToolRequestSchema,async req=>{
66
+ if(req.params.name===websiteReturnTool.name){const args=req.params.arguments||{};if(Object.keys(args).some(k=>k!=='waitSeconds'))throw Error('Return lookup accepts a bounded wait only');const result=attachment?await attachment.receive(args):{state:'unavailable'};if(result.state?.startsWith('received')&&pendingChooser)pendingChooser={...pendingChooser,structuredContent:{...pendingChooser.structuredContent,authenticatedReturn:result}};return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
64
67
  if(req.params.name===pendingChooserTool.name){if(Object.keys(req.params.arguments||{}).length)throw Error('Pending chooser lookup takes no identity or credential fields');return pendingChooserResponse(pendingChooser);}
65
68
  if(req.params.name!=='show_visual_setup_choices')return invoke('callTool',req.params);
66
69
  const argumentsValue={...(req.params.arguments||{})};
@@ -68,6 +71,10 @@ export async function serveRemote(options={}){
68
71
  if(Object.hasOwn(argumentsValue,'goal')){if(typeof argumentsValue.goal!=='string'||!argumentsValue.goal.trim()||argumentsValue.goal.length>1000)throw Error('Use a presentation request of 1-1000 characters');nextGoal=argumentsValue.goal;delete argumentsValue.goal;}
69
72
  const result=await invoke('callTool',{...req.params,arguments:argumentsValue});
70
73
  const decorated=chooserResponse(result,{apps:!!forwardedCapabilities(server.getClientCapabilities()).experimental,goal:nextGoal});
74
+ if(!decorated.isError&&nextGoal&&attachment){
75
+ try{const link=await attachment.open(nextGoal),priorUrl=decorated.structuredContent.chooser.url;decorated.content=decorated.content.map(c=>c.type==='text'&&c.text.startsWith('Website chooser: ')?{...c,text:c.text.replace(priorUrl,link.chooserUrl)}:c);decorated.structuredContent.chooser={...decorated.structuredContent.chooser,url:link.chooserUrl,returnMethod:'paired-encrypted-website-selection'};decorated.content.push({type:'text',text:'Open '+link.chooserUrl+' in your signed-in account. Confirm the waiting IDE code '+link.pairCode+' and the existing project. Then return the selection; receive_website_selection waits on this same MCP connection. The locator grants no account authority.'});decorated.structuredContent.websiteReturn={...link,extensionRequired:false,engineAuthority:false};}
76
+ catch{decorated.structuredContent.websiteReturn={available:false,manualManifestFallback:true};}
77
+ }
71
78
  if(!decorated.isError){requestGoal=nextGoal;pendingChooser=decorated;}return decorated;
72
79
  });
73
80
  }
@@ -77,6 +84,6 @@ export async function serveRemote(options={}){
77
84
  server.setRequestHandler(ReadResourceRequestSchema,req=>invoke('readResource',req.params));
78
85
  server.setRequestHandler(ListResourceTemplatesRequestSchema,req=>invoke('listResourceTemplates',req.params));
79
86
  }
80
- server.onclose=()=>{closed=true;void ready.catch(()=>{}).then(()=>remote.close());};
87
+ server.onclose=()=>{closed=true;attachment?.close?.();void ready.catch(()=>{}).then(()=>remote.close());};
81
88
  await server.connect(options.transport??new StdioServerTransport());return server;
82
89
  }
@@ -0,0 +1,18 @@
1
+ // Delivery keys protect inert presentation data. They never authorize engine calls.
2
+ import {generateKeyPairSync,createPublicKey,diffieHellman,hkdfSync,createDecipheriv,createHash} from 'node:crypto';
3
+ const version='kloudy.website-return/1',hash=s=>createHash('sha256').update(s).digest('hex'),origin='https://kloudy.ai',root=origin+'/api/customer/visual-chooser/return/';
4
+ export const websiteReturnTool={name:'receive_website_selection',description:'Wait for the exact inert selection returned by the signed-in website to this MCP connection. No IDE extension, website cookie, account token, model or grant is needed. Use after showing the chooser link. A waiting result is safe to check again; do not repeat mutations after uncertain outcomes.',inputSchema:{type:'object',additionalProperties:false,properties:{waitSeconds:{type:'integer',minimum:0,maximum:45}}},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
+ export class WebsiteSelectionReturn{
6
+ constructor({fetcher=fetch,clock=()=>Date.now(),sleep=ms=>new Promise(r=>setTimeout(r,ms))}={}){Object.assign(this,{fetcher,clock,sleep});this.keys=generateKeyPairSync('x25519');this.publicKey=this.keys.publicKey.export({format:'jwk'}).x;this.sequence=0;this.receipt=null;}
7
+ async request(action,input){const response=await this.fetcher(root+action,{method:'POST',headers:{origin,'content-type':'application/json'},body:JSON.stringify(input),redirect:'error',signal:AbortSignal.timeout(10000)});const text=await response.text();if(text.length>300000)throw Error('Return response exceeds the bounded manifest');const value=JSON.parse(text);if(!response.ok)throw Error(typeof value.error==='string'?value.error:'Website return unavailable');return value;}
8
+ async open(goal){if(this.link)return this.link;if(typeof goal!=='string'||!goal.trim()||goal.length>1000)throw Error('Use a presentation goal up to 1000 characters');const result=await this.request('open',{publicKey:this.publicKey,goal});if(result.version!==version||!/^[a-f0-9]{32}$/.test(result.id)||result.linkIsAuthorization!==false||result.chooserUrl!==origin+'/api/customer/visual-chooser/start#return='+result.id||result.pairCode!==hash(result.id+':'+this.publicKey).slice(0,8).toUpperCase())throw Error('Unexpected website return attachment');this.link=result;return result;}
9
+ decrypt(envelope){
10
+ if(envelope?.version!==version||envelope.id!==this.link?.id||envelope.sequence!==this.sequence+1||!/^[A-Za-z0-9_-]{43}$/.test(envelope.serverPublicKey)||!/^[A-Za-z0-9_-]{16}$/.test(envelope.iv)||!/^[A-Za-z0-9_-]{22}$/.test(envelope.tag)||typeof envelope.ciphertext!=='string'||envelope.ciphertext.length>270000)throw Error('Unexpected return envelope');
11
+ const aad=JSON.stringify([version,this.link.id,hash(this.publicKey),envelope.sequence]),peer=createPublicKey({key:{kty:'OKP',crv:'X25519',x:envelope.serverPublicKey},format:'jwk'}),secret=diffieHellman({privateKey:this.keys.privateKey,publicKey:peer}),key=Buffer.from(hkdfSync('sha256',secret,Buffer.alloc(0),Buffer.from(aad),32)),decipher=createDecipheriv('aes-256-gcm',key,Buffer.from(envelope.iv,'base64url'));decipher.setAAD(Buffer.from(aad));decipher.setAuthTag(Buffer.from(envelope.tag,'base64url'));const payload=JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext,'base64url')),decipher.final()]).toString()),s=payload.result?.structuredContent,m=s?.selectedManifest;
12
+ if(payload.version!==version||payload.accountProjectVerified!==true||payload.executionAllowed!==false||typeof payload.conversationId!=='string'||typeof payload.receiptId!=='string'||payload.projectId!==m?.projectId||payload.projectId!==s?.projectState?.projectId||payload.revision!==s.revision||m.revision!==s.revision||s.executed!==false||m.executionAllowed!==false||m.authorizesSpending!==false||m.authorizesInstallation!==false)throw Error('Website selection is not an exact inert project manifest');
13
+ if(this.receipt&&(payload.projectId!==this.receipt.projectId||payload.conversationId!==this.receipt.conversationId||s.projectState.originalGoal!==this.receipt.result.structuredContent.projectState.originalGoal))throw Error('Account project or original conversation changed');
14
+ if(typeof payload.ackNonce!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(payload.ackNonce))throw Error('Missing exact delivery acknowledgment');this.pendingAck={id:this.link.id,sequence:envelope.sequence,nonce:payload.ackNonce};delete payload.ackNonce;this.sequence=envelope.sequence;this.receipt=payload;return payload;
15
+ }
16
+ async receive({waitSeconds=45}={}){if(!Number.isInteger(waitSeconds)||waitSeconds<0||waitSeconds>45)throw Error('Wait up to 45 seconds');if(!this.link)return {state:'not-open',instructions:'Show the visual chooser with your goal first.'};if(this.pendingAck){await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...this.receipt};}const end=this.clock()+waitSeconds*1000;do{const value=await this.request('poll',{id:this.link.id,afterSequence:this.sequence});if(value.version!==version||!['waiting','ready'].includes(value.state))throw Error('Unknown website return state');if(value.state==='ready'){const receipt=this.decrypt(value.envelope);try{await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...receipt};}catch{return {state:'received-acknowledgment-unconfirmed',...receipt};}}if(this.clock()>=end)break;await this.sleep(Math.min(2000,end-this.clock()));}while(this.clock()<=end);return {state:'waiting',chooserUrl:this.link.chooserUrl,pairCode:this.link.pairCode,conversationId:this.receipt?.conversationId??null,lastReceipt:this.receipt??null};}
17
+ close(){this.keys=null;this.link=null;this.receipt=null;}
18
+ }