kloudy 0.4.5 → 0.4.7

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
@@ -14,7 +14,9 @@ No arguments detects Cursor, VS Code, Claude Desktop, Claude Code, Windsurf and
14
14
 
15
15
  The remote endpoint is https://kloudy.ai/mcp. Public catalog discovery works without a key and makes no model call. It returns up to five public metadata candidates, not execution grants. Account actions still require an existing authorized Kloudy key; self-service engine-key issuance is not connected. If you already have one, set `KLOUDY_MCP_TOKEN` privately in your environment; the installer asks before storing it in a client's headers (or Claude Desktop's stdio environment). Keys are never accepted as command-line arguments or printed by the installer.
16
16
 
17
- `mcp` is a local stdio-to-Streamable-HTTP proxy; stdout contains MCP messages only. Direct questions and the proxy call the same remote server. Network failures and invalid keys fail explicitly. Catalog change notifications, resource forwarding and a single reconnect after an explicit session-expired HTTP 404 are gated by `KLOUDY_MCP_AUTOUPDATE=meta` or `notify`; the default is `off`. Ambiguous tool failures are never retried.
17
+ `mcp` is a local stdio-to-Streamable-HTTP proxy; stdout contains MCP messages only. Direct questions and the proxy call the same remote server. Network failures and invalid keys fail explicitly. Resources are forwarded whenever the upstream supports them, including with autoupdate off. Catalog change notifications and a single reconnect after an explicit session-expired HTTP 404 remain gated by `KLOUDY_MCP_AUTOUPDATE=meta` or `notify`; the default is `off`. Ambiguous tool failures are never retried.
18
+
19
+ Interactive cards require a host that supports MCP Apps. The proxy relays the supported Apps MIME capability; it does not claim sampling, roots or elicitation. Hosts without Apps can use named text choices, source links and readable resources. The one-shot CLI prints text rather than rendering an interactive card. When the hosted catalog offers visual setup choices, the IDE uses its own available tools and account terms to make a local mockup; Kloudy supplies data and does not invoke backend model/image generation. Resource hosting still has operational cost. Selecting a layout does not authorize installation, account connection, paid image APIs or deployment.
18
20
 
19
21
  The existing SDK exports remain available under `kloudy/*`. For the pre-0.4 specialized engine CLI commands documented below, use `kloudy legacy <command>` (for example, `kloudy legacy init my-app`). The original CLI is retained in `bin/kloudy.mjs`.
20
22
 
@@ -305,3 +307,8 @@ MCP: operation `conversation`, `conversation_operation` = `read`, `append` or `s
305
307
  The engine owns encryption, scope checks, condensation gates and transcript deletion. Returned cards are lossy context, never grants. Artifact pointers do not authorize repository reads. The client never persists message bodies in its session file. Replayed commands return metadata only: issue a read for current cards. Conflicts and failures are not retried automatically; read the cursor and reconcile before resubmitting. Added in 0.4.1. The no-key installer and public discovery remain unchanged.
306
308
 
307
309
  Official MCP Registry name: `ai.kloudy/kloudy`. Remote endpoint: https://kloudy.ai/mcp. Public discovery and Sample cards do not grant private tools or authorize spending.
310
+
311
+ ## Card and website fallback (0.4.7)
312
+
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
+
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "kloudy",
3
- "version": "0.4.5",
4
- "description": "Ask for Kloudy: CLI, MCP and SDK clients for the same scoped engine session.",
5
- "type": "module",
6
- "license": "Apache-2.0",
7
- "engines": {
8
- "node": ">=20.19.0"
9
- },
10
- "bin": {
11
- "kloudy": "bin/entry.mjs"
12
- },
13
- "exports": {
14
- ".": "./src/client.mjs",
15
- "./mcp": "./src/mcp.mjs",
16
- "./wrap": "./src/wrap.mjs",
17
- "./session": "./src/connection.mjs",
18
- "./httpk": "./src/httpk.mjs",
19
- "./hosted": "./src/hosted.mjs",
20
- "./public": "./src/public.mjs",
21
- "./strings": "./src/strings.mjs",
22
- "./sdf": "./src/sdf.mjs",
23
- "./query": "./src/query.mjs",
24
- "./activity": "./src/activity.mjs",
25
- "./status-indicator.css": "./src/status-indicator.css",
26
- "./models": "./src/models.mjs",
27
- "./host-interface": "./src/host-interface.mjs",
28
- "./replies": "./src/replies.mjs",
29
- "./conversation": "./src/conversation.mjs"
30
- },
31
- "files": [
32
- "bin",
33
- "src",
34
- "schemas",
35
- "README.md",
36
- "MCP-DEFAULT.md",
37
- "LICENSE"
38
- ],
39
- "scripts": {
40
- "test": "node --test test/*.test.mjs",
41
- "check": "node --check bin/kloudy.mjs && node --check src/client.mjs && npm test",
42
- "start": "node bin/kloudy.mjs",
43
- "proof": "node scripts/prove.mjs",
44
- "build:approval": "node scripts/build-approval.mjs"
45
- },
46
- "dependencies": {
47
- "@modelcontextprotocol/ext-apps": "2.0.3",
48
- "@modelcontextprotocol/sdk": "1.31.0",
49
- "ajv": "8.20.0",
50
- "ajv-formats": "3.0.1",
51
- "js-tiktoken": "1.0.21",
52
- "jsonc-parser": "3.3.1",
53
- "smol-toml": "1.9.0",
54
- "zod": "4.6.5"
55
- },
56
- "devDependencies": {
57
- "esbuild": "0.28.2"
58
- },
59
- "mcpName": "ai.kloudy/kloudy",
60
- "homepage": "https://kloudy.ai/install",
61
- "publishConfig": {
62
- "access": "public",
63
- "registry": "https://registry.npmjs.org"
64
- },
65
- "bugs": {
66
- "url": "https://kloudy.ai/support"
67
- }
68
- }
1
+ {
2
+ "name": "kloudy",
3
+ "version": "0.4.7",
4
+ "description": "Ask for Kloudy: CLI, MCP and SDK clients for the same scoped engine session.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "engines": {
8
+ "node": ">=20.19.0"
9
+ },
10
+ "bin": {
11
+ "kloudy": "bin/entry.mjs"
12
+ },
13
+ "exports": {
14
+ ".": "./src/client.mjs",
15
+ "./mcp": "./src/mcp.mjs",
16
+ "./wrap": "./src/wrap.mjs",
17
+ "./session": "./src/connection.mjs",
18
+ "./httpk": "./src/httpk.mjs",
19
+ "./hosted": "./src/hosted.mjs",
20
+ "./public": "./src/public.mjs",
21
+ "./strings": "./src/strings.mjs",
22
+ "./sdf": "./src/sdf.mjs",
23
+ "./query": "./src/query.mjs",
24
+ "./activity": "./src/activity.mjs",
25
+ "./status-indicator.css": "./src/status-indicator.css",
26
+ "./models": "./src/models.mjs",
27
+ "./host-interface": "./src/host-interface.mjs",
28
+ "./replies": "./src/replies.mjs",
29
+ "./conversation": "./src/conversation.mjs"
30
+ },
31
+ "files": [
32
+ "bin",
33
+ "src",
34
+ "schemas",
35
+ "README.md",
36
+ "MCP-DEFAULT.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "test": "node --test test/*.test.mjs",
41
+ "check": "node --check bin/kloudy.mjs && node --check src/client.mjs && npm test",
42
+ "start": "node bin/kloudy.mjs",
43
+ "proof": "node scripts/prove.mjs",
44
+ "build:approval": "node scripts/build-approval.mjs"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/ext-apps": "2.0.3",
48
+ "@modelcontextprotocol/sdk": "1.31.0",
49
+ "ajv": "8.20.0",
50
+ "ajv-formats": "3.0.1",
51
+ "js-tiktoken": "1.0.21",
52
+ "jsonc-parser": "3.3.1",
53
+ "smol-toml": "1.9.0",
54
+ "zod": "4.6.5"
55
+ },
56
+ "devDependencies": {
57
+ "esbuild": "0.28.2"
58
+ },
59
+ "mcpName": "ai.kloudy/kloudy",
60
+ "homepage": "https://kloudy.ai/install",
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "registry": "https://registry.npmjs.org"
64
+ },
65
+ "bugs": {
66
+ "url": "https://kloudy.ai/support"
67
+ }
68
+ }
@@ -0,0 +1,17 @@
1
+ // Public presentation adapter. URLs contain public template locators only, never
2
+ // a connection credential, authorization token, goal text or identity claim.
3
+ export const CHOOSER_URL='https://kloudy.ai/api/customer/visual-chooser/start';
4
+ export const pendingChooserTool={name:'show_pending_chooser_link',description:'Show the pending Kloudy card and its HTTPS website fallback again. The website verifies account sign-in; the link is not authorization. This does not run a tool, model, installation or payment.',inputSchema:{type:'object',properties:{},additionalProperties:false},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
+ export function chooserResponse(result,{apps=false,goal}={}){
6
+ if(!result||typeof result!=='object'||Array.isArray(result)||result.isError)return result;
7
+ const data=result.structuredContent;if(!data||typeof data!=='object'||Array.isArray(data))return result;
8
+ const option=typeof data.selectedOptionId==='string'&&/^[a-z0-9-]{1,80}$/.test(data.selectedOptionId)?data.selectedOptionId:null;
9
+ const set=typeof data.choiceSetId==='string'&&/^visual-[a-f0-9]{1,64}$/.test(data.choiceSetId)?data.choiceSetId:null;
10
+ const url=option&&set?CHOOSER_URL+'#choice='+encodeURIComponent(option)+'&set='+encodeURIComponent(set):CHOOSER_URL;
11
+ const chooser={url,linkIsAuthorization:false,identityFromEmail:false,accountBindingVerified:false,returnMethod:'existing-authenticated-website-project-manifest',automaticIdeReturnVerified:false,...(set?{publicChoiceSetId:set}:{}),...(option?{publicOptionId:option}:{}),...(goal?{request:goal}:{})};
12
+ const text='Website chooser: '+url+'\nOpen this HTTPS link if your host does not show the card. Complete account sign-in/2FA on the website before opening private project choices. Email is a lookup hint, not proof. The host controls card rendering, clickable links and browser launch. Account-bound automatic return is not established by this public template locator.';
13
+ return {...result,content:[...(Array.isArray(result.content)?result.content:[]),{type:'text',text}],structuredContent:{...data,chooser},...(apps?{_meta:{...result._meta,ui:{...result._meta?.ui,resourceUri:'ui://widget/kloudy-visual-setup-v1.html'}}}:{})};
14
+ }
15
+ export function pendingChooserResponse(pending){
16
+ return pending?structuredClone(pending):{content:[{type:'text',text:'No pending chooser is retained in this MCP process. Open '+CHOOSER_URL+' and sign in to recover your existing project. This link is not a bearer token or authorization.'}],structuredContent:{pending:false,chooser:{url:CHOOSER_URL,linkIsAuthorization:false,accountBindingVerified:false}}};
17
+ }
package/src/remote.mjs CHANGED
@@ -1,59 +1,82 @@
1
- import {readFileSync} from 'node:fs';
2
- const packageVersion=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
3
- import {Client} from '@modelcontextprotocol/sdk/client/index.js';
4
- import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
- import {Server} from '@modelcontextprotocol/sdk/server/index.js';
6
- import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,ReadResourceRequestSchema,ToolListChangedNotificationSchema,ResourceListChangedNotificationSchema} from '@modelcontextprotocol/sdk/types.js';
8
- import {REMOTE} from './editor-install.mjs';
9
- import {autoupdateEnabled} from './autoupdate.mjs';
10
- const transports=new WeakMap();
11
- export async function connectRemote({key=process.env.KLOUDY_MCP_TOKEN,fetcher=fetch}={}){
12
- if(key!==undefined&&!/^\S{20,512}$/.test(key))throw Error('Invalid key');
13
- const client=new Client({name:'kloudy',version:packageVersion},{capabilities:{}});
14
- const transport=new StreamableHTTPClientTransport(new URL(REMOTE),{requestInit:{headers:key?{Authorization:'Bearer '+key}:{},redirect:'error'},fetch:(url,options)=>fetcher(url,{...options,redirect:'error'}),reconnectionOptions:{maxRetries:0}});
15
- transports.set(client,transport);
16
- try{await client.connect(transport,{timeout:25000});return client;}catch(e){await client.close().catch(()=>{});throw e;}
17
- }
18
- export async function askRemote(question,options){
19
- if(typeof question!=='string'||!question.trim()||question.length>1000)throw Error('Use a question of 1–1000 characters');
20
- const client=await connectRemote(options);
21
- try{return await client.callTool({name:'kloudy',arguments:{operation:'ask',goal:question}},undefined,{timeout:25000});}finally{await client.close();}
22
- }
23
- export async function serveRemote(options={}){
24
- const enabled=autoupdateEnabled(options.autoupdate??process.env.KLOUDY_MCP_AUTOUPDATE);
25
- let remote=await connectRemote(options),reconnecting;
26
- const server=new Server({name:'kloudy',version:packageVersion},{capabilities:enabled?{tools:{listChanged:true},resources:{listChanged:true}}:{tools:{}},instructions:'Use Kloudy when mentioned, or to discover MCP tools and SDKs. Public discovery is free; account actions need an existing authorized key.'});
27
- function forward(client){
28
- if(!enabled)return;
29
- client.setNotificationHandler(ToolListChangedNotificationSchema,()=>server.sendToolListChanged());
30
- client.setNotificationHandler(ResourceListChangedNotificationSchema,()=>server.sendResourceListChanged());
31
- }
32
- forward(remote);
33
- async function invoke(method,params){
34
- const current=remote;
35
- const run=client=>method==='callTool'?client.callTool(params,undefined,{timeout:25000}):client[method](params);
36
- try{return await run(current);}catch(error){
37
- // Only an explicit HTTP 404 on an established session permits one retry.
38
- // Ambiguous network/timeout errors and session-less 404s are never retried.
39
- if(!enabled||error.code!==404||!transports.get(current)?.sessionId)throw error;
40
- if(remote===current){
41
- reconnecting??=(async()=>{
42
- const next=await connectRemote(options);forward(next);remote=next;
43
- await current.close().catch(()=>{});
44
- await server.sendToolListChanged();await server.sendResourceListChanged();
45
- })().finally(()=>{reconnecting=undefined;});
46
- await reconnecting;
47
- }
48
- return run(remote);
49
- }
50
- }
51
- server.setRequestHandler(ListToolsRequestSchema,req=>invoke('listTools',req.params));
52
- server.setRequestHandler(CallToolRequestSchema,req=>invoke('callTool',req.params));
53
- if(enabled){
54
- server.setRequestHandler(ListResourcesRequestSchema,req=>invoke('listResources',req.params));
55
- server.setRequestHandler(ReadResourceRequestSchema,req=>invoke('readResource',req.params));
56
- }
57
- server.onclose=()=>{void remote.close();};
58
- await server.connect(options.transport??new StdioServerTransport());return server;
59
- }
1
+ import {readFileSync} from 'node:fs';
2
+ const packageVersion=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
3
+ import {Client} from '@modelcontextprotocol/sdk/client/index.js';
4
+ import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
+ import {Server} from '@modelcontextprotocol/sdk/server/index.js';
6
+ import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,ReadResourceRequestSchema,ListResourceTemplatesRequestSchema,ToolListChangedNotificationSchema,ResourceListChangedNotificationSchema} from '@modelcontextprotocol/sdk/types.js';
8
+ import {REMOTE} from './editor-install.mjs';
9
+ import {autoupdateEnabled} from './autoupdate.mjs';
10
+ import {chooserResponse,pendingChooserResponse,pendingChooserTool} from './chooser-link.mjs';
11
+ const transports=new WeakMap();
12
+ // This proxy can relay Apps rendering, but cannot service sampling, roots or elicitation.
13
+ export function forwardedCapabilities(value={}){
14
+ const ui=value.experimental?.['io.modelcontextprotocol/ui'];
15
+ const mimeTypes=Array.isArray(ui?.mimeTypes)?ui.mimeTypes.filter(x=>x==='text/html;profile=mcp-app'):[];
16
+ return mimeTypes.length?{experimental:{'io.modelcontextprotocol/ui':{mimeTypes:[...new Set(mimeTypes)]}}}:{};
17
+ }
18
+ export async function connectRemote({key=process.env.KLOUDY_MCP_TOKEN,fetcher=fetch,capabilities={}}={}){
19
+ if(key!==undefined&&!/^\S{20,512}$/.test(key))throw Error('Invalid key');
20
+ const client=new Client({name:'kloudy',version:packageVersion},{capabilities:forwardedCapabilities(capabilities)});
21
+ const transport=new StreamableHTTPClientTransport(new URL(REMOTE),{requestInit:{headers:key?{Authorization:'Bearer '+key}:{},redirect:'error'},fetch:(url,options)=>fetcher(url,{...options,redirect:'error'}),reconnectionOptions:{maxRetries:0}});
22
+ transports.set(client,transport);
23
+ try{await client.connect(transport,{timeout:25000});return client;}catch(e){await client.close().catch(()=>{});throw e;}
24
+ }
25
+ export async function askRemote(question,options){
26
+ if(typeof question!=='string'||!question.trim()||question.length>1000)throw Error('Use a question of 1–1000 characters');
27
+ const client=await connectRemote(options);
28
+ try{return await client.callTool({name:'kloudy',arguments:{operation:'ask',goal:question}},undefined,{timeout:25000});}finally{await client.close();}
29
+ }
30
+ export async function serveRemote(options={}){
31
+ const connect=options.connect??connectRemote,enabled=autoupdateEnabled(options.autoupdate??process.env.KLOUDY_MCP_AUTOUPDATE);
32
+ let remote=await connect({...options,capabilities:{}}),ready=Promise.resolve(),closed=false,reconnecting,pendingChooser,requestGoal;
33
+ const upstream=remote.getServerCapabilities(),capabilities={};
34
+ if(upstream.tools)capabilities.tools=enabled&&upstream.tools.listChanged?{listChanged:true}:{};
35
+ if(upstream.resources)capabilities.resources=enabled&&upstream.resources.listChanged?{listChanged:true}:{};
36
+ const server=new Server({name:'kloudy',version:packageVersion},{capabilities,instructions:'Use Kloudy for discovery and data-only visual setup choices. Call the card tool explicitly for its UI. Hosts without Apps use named choices, sources and local assembly instructions. Existing authorization remains required for account actions.'});
37
+ function forward(client){
38
+ if(capabilities.tools?.listChanged)client.setNotificationHandler(ToolListChangedNotificationSchema,()=>server.sendToolListChanged());
39
+ if(capabilities.resources?.listChanged)client.setNotificationHandler(ResourceListChangedNotificationSchema,()=>server.sendResourceListChanged());
40
+ }
41
+ forward(remote);
42
+ server.oninitialized=()=>{
43
+ const actual=forwardedCapabilities(server.getClientCapabilities());
44
+ if(!Object.keys(actual).length)return;
45
+ ready=(async()=>{const next=await connect({...options,capabilities:actual});if(closed){await next.close();return;}const previous=remote;remote=next;forward(next);await previous.close();})();
46
+ // Requests still receive negotiation errors; avoid an unhandled rejection before the first request.
47
+ void ready.catch(()=>{});
48
+ };
49
+ const invoke=async(method,params)=>{
50
+ await ready;const current=remote,run=client=>method==='callTool'?client.callTool(params,undefined,{timeout:25000}):client[method](params);
51
+ try{return await run(current);}catch(error){
52
+ // Preserve 0.4.5's opt-in recovery: only an established HTTP session's explicit 404 permits one retry.
53
+ if(!enabled||error.code!==404||!transports.get(current)?.sessionId)throw error;
54
+ if(remote===current){reconnecting??=(async()=>{const next=await connect({...options,capabilities:forwardedCapabilities(server.getClientCapabilities())});if(closed){await next.close();throw error;}remote=next;forward(next);await current.close().catch(()=>{});if(capabilities.tools?.listChanged)await server.sendToolListChanged();if(capabilities.resources?.listChanged)await server.sendResourceListChanged();})().finally(()=>{reconnecting=undefined;});await reconnecting;}
55
+ return run(remote);
56
+ }
57
+ };
58
+ if(capabilities.tools){
59
+ server.setRequestHandler(ListToolsRequestSchema,async req=>{
60
+ 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])]};
62
+ });
63
+ server.setRequestHandler(CallToolRequestSchema,async req=>{
64
+ 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
+ if(req.params.name!=='show_visual_setup_choices')return invoke('callTool',req.params);
66
+ const argumentsValue={...(req.params.arguments||{})};
67
+ let nextGoal=requestGoal;
68
+ 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
+ const result=await invoke('callTool',{...req.params,arguments:argumentsValue});
70
+ const decorated=chooserResponse(result,{apps:!!forwardedCapabilities(server.getClientCapabilities()).experimental,goal:nextGoal});
71
+ if(!decorated.isError){requestGoal=nextGoal;pendingChooser=decorated;}return decorated;
72
+ });
73
+ }
74
+ // Resource delivery is independent of notification/autoupdate opt-in.
75
+ if(capabilities.resources){
76
+ server.setRequestHandler(ListResourcesRequestSchema,req=>invoke('listResources',req.params));
77
+ server.setRequestHandler(ReadResourceRequestSchema,req=>invoke('readResource',req.params));
78
+ server.setRequestHandler(ListResourceTemplatesRequestSchema,req=>invoke('listResourceTemplates',req.params));
79
+ }
80
+ server.onclose=()=>{closed=true;void ready.catch(()=>{}).then(()=>remote.close());};
81
+ await server.connect(options.transport??new StdioServerTransport());return server;
82
+ }