kloudy 0.4.3 → 0.4.5
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/MCP-DEFAULT.md +12 -12
- package/README.md +307 -307
- package/bin/entry.mjs +22 -20
- package/bin/kloudy.mjs +70 -70
- package/package.json +68 -68
- package/schemas/SDF-LICENSE.txt +21 -21
- package/schemas/sdf-document-0.2.schema.json +411 -411
- package/schemas/sdf-webpage-v1.schema.json +176 -176
- package/src/activity.mjs +23 -23
- package/src/approval.mjs +25 -25
- package/src/autoupdate.mjs +2 -0
- package/src/client.mjs +96 -96
- package/src/connect-card.mjs +12 -15
- package/src/connect-ui.mjs +2 -1
- package/src/connection.mjs +14 -14
- package/src/editor-install.mjs +22 -4
- package/src/hosted.mjs +40 -35
- package/src/httpk.mjs +64 -64
- package/src/install.mjs +35 -35
- package/src/introduce.mjs +33 -33
- package/src/mcp.mjs +72 -72
- package/src/models.mjs +55 -55
- package/src/preferences-card.mjs +21 -0
- package/src/preferences-ui.mjs +2 -0
- package/src/public.mjs +41 -41
- package/src/remote.mjs +42 -8
- package/src/sample-cards.mjs +6 -6
- package/src/sample-ui.mjs +1 -1
- package/src/starter.mjs +43 -43
- package/src/strings.mjs +51 -51
- package/src/wrap.mjs +58 -58
package/src/mcp.mjs
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
1
|
import {conversationParams,conversationResult} from './conversation.mjs';
|
|
2
|
-
import {instrumentToolHandler} from './activity.mjs';
|
|
3
|
-
import {configuredModels} from './models.mjs';
|
|
4
|
-
import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
|
|
5
|
-
import {welcomeContent} from './welcome.mjs';
|
|
6
|
-
import {Approvals,approvalTool,approvalResource} from './approval.mjs';
|
|
7
|
-
import {approvalHTML} from './approval-ui.mjs';
|
|
8
|
-
import {getUiCapability,RESOURCE_MIME_TYPE} from '@modelcontextprotocol/ext-apps/server';
|
|
9
|
-
import {convertPage,convertPages} from './public.mjs';
|
|
10
|
-
import {localStrings} from './strings.mjs';
|
|
11
|
-
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
-
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
-
import {ListToolsRequestSchema,CallToolRequestSchema,ListPromptsRequestSchema,GetPromptRequestSchema,ListResourcesRequestSchema,ReadResourceRequestSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
-
import {fileURLToPath} from 'node:url';
|
|
15
|
-
import {introduce,isAttachIntent,ideGuidance} from './introduce.mjs';
|
|
16
|
-
import {withConnection} from './connection.mjs';
|
|
17
|
-
const gateway={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. For Kloudy or /kloudy alone, use introduce to welcome this project; an attached host does not need another install question. Select one tool through the same engine. Autonomous by default; the IDE permission system controls tool calls and the engine enforces authorization.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['conversation','host_interface','models_list','action','wallet_connection','hosting','wallet','introduce','decline','convert','yield_list','query','ask','assemble','call','status','cancel','strings_list','strings_save','strings_call','strings_use','strings_keep','strings_remove','strings_prune']},conversation_operation:{type:'string',enum:['read','append','sync']},conversation:{type:'object'},action:{type:'string',enum:['chain.head','wallet.prepare','bww.fetch','bww.publish']},inputs:{type:'object'},idempotency_key:{type:'string',maxLength:128},chain:{type:'string',maxLength:100},budget_usd_micros:{type:'integer',minimum:1,maximum:1000000000000},wallet_operation:{type:'string',enum:['balance','top_up']},amount_usd:{type:'string'},goal:{type:'string'},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},url:{type:'string'},urls:{type:'array',minItems:1,maxItems:8,items:{type:'string'}},resolution:{type:'string',enum:['compact','standard']},string_name:{type:'string'},choices:{type:'array',maxItems:16,items:{type:'object',properties:{source:{type:'string'},kind:{type:'string',enum:['mcp','sdk','cli']},goal:{type:'string'}},required:['source','kind','goal'],additionalProperties:false}},selection_ids:{type:'array',items:{type:'string'},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},name:{type:'string'},arguments:{type:'object'},candidates:{type:'array',maxItems:16,items:{type:'object'}},lane:{type:'string',enum:['non_medical','medical']}},additionalProperties:false}};
|
|
18
|
-
export function createMcpServer({useClient,greet=introduce,strings=localStrings,models=configuredModels,onActivity}){
|
|
19
|
-
const server=new Server({name:'kloudy',version:'0.3.15'},{capabilities:{tools:{listChanged:true},prompts:{},resources:{}},instructions:'When the user says Kloudy or /kloudy, use operation introduce. Return the current project greeting. Do not repeatedly ask to install an already attached tool. Respect a no answer and the host permission ceiling. '+Object.values(ideGuidance).join(' ')});
|
|
20
|
-
const approvals=new Approvals(),hasUI=()=>getUiCapability(server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE);
|
|
21
|
-
server.setRequestHandler(ListResourcesRequestSchema,async()=>({resources:hasUI()?[{uri:approvalResource,name:'Kloudy action approval',mimeType:RESOURCE_MIME_TYPE}]:[]}));
|
|
22
|
-
server.setRequestHandler(ReadResourceRequestSchema,async request=>{if(!hasUI()||request.params.uri!==approvalResource)throw Error('Unsupported UI resource');return {contents:[{uri:approvalResource,mimeType:RESOURCE_MIME_TYPE,text:approvalHTML,_meta:{ui:{csp:{connectDomains:[],resourceDomains:[]}}}}]};});
|
|
23
|
-
async function projectRoot(){try{const roots=await server.listRoots();if(roots.roots?.[0]?.uri?.startsWith('file:'))return fileURLToPath(roots.roots[0].uri);}catch{}return process.env.CLAUDE_PROJECT_DIR||process.cwd();}
|
|
24
|
-
async function greeting(decision){let project=process.env.CLAUDE_PROJECT_DIR||process.cwd();try{const roots=await server.listRoots();if(roots.roots?.[0]?.uri?.startsWith('file:'))project=fileURLToPath(roots.roots[0].uri);}catch{}return greet({project,decision,hostAttached:true});}
|
|
25
|
-
server.setRequestHandler(ListPromptsRequestSchema,async()=>({prompts:[{name:'kloudy',description:'Welcome the current project to Kloudy and explain how to use it.'}]}));
|
|
26
|
-
server.setRequestHandler(GetPromptRequestSchema,async request=>{if(request.params.name!=='kloudy')throw Error('Unknown prompt');const result=await greeting();return {description:'Ask for Kloudy',messages:[{role:'assistant',content:welcomeContent(result)[0]}]};});
|
|
27
|
-
server.setRequestHandler(ListToolsRequestSchema,async()=>{
|
|
28
|
-
let selected;try{selected=await useClient(client=>client.state.selection?.tool);}catch{}
|
|
29
|
-
const meta=hasUI()?{_meta:{ui:{resourceUri:approvalResource}}}:{};return {tools:[{...gateway,...meta},...(selected?[{...selected,name:'selected_'+selected.name,...meta}]:[]),...(hasUI()?[approvalTool]:[])]};
|
|
30
|
-
});
|
|
31
|
-
server.setRequestHandler(CallToolRequestSchema,instrumentToolHandler(async request=>{
|
|
32
|
-
try{
|
|
33
|
-
const supplied=request.params.arguments||{};
|
|
34
|
-
if(request.params.name==='kloudy'&&supplied.operation==='models_list'){if(Object.keys(supplied).length!==1)throw Error('Model discovery uses host configuration only');const result=await models().list();return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
35
|
-
|
|
36
|
-
if(request.params.name==='kloudy'&&['convert','yield_list'].includes(supplied.operation)){const result=await (supplied.operation==='convert'?convertPage(supplied.url,{resolution:supplied.resolution}):convertPages(supplied.urls,{resolution:supplied.resolution}));return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
37
|
-
if(request.params.name==='kloudy_approval'){if(!hasUI())throw Error('This host has no inline approval UI');const result=await approvals.decide(supplied,useClient);return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
38
|
-
if(request.params.name==='kloudy'&&supplied.operation?.startsWith('strings_')){const store=strings({project:await projectRoot(),mode:'auto'}),op=supplied.operation.slice(8);let result;
|
|
39
|
-
if(op==='save')result=await store.save(supplied.string_name,supplied.choices);
|
|
40
|
-
else if(op==='call'){result=await store.call(supplied.string_name,supplied.name,supplied.arguments,{selectionId:supplied.selection_id});if(result?.state==='awaiting_approval'){const saved=(await store.list()).find(s=>s.name===supplied.string_name),route=saved?.activeRoutes?.find(r=>r.name===supplied.name&&(!supplied.selection_id||r.selection_id===supplied.selection_id));if(!route)throw Error('Selected route unavailable');return approvals.present(result,route.selection_id,work=>store.connect(route.source,route.kind,work));}}
|
|
41
|
-
else if(op==='list')result=await store.list();
|
|
42
|
-
else if(op==='use'){result=await store.use(supplied.string_name);}
|
|
43
|
-
else if(['keep','remove'].includes(op))result=await store[op](supplied.string_name);
|
|
44
|
-
else if(op==='prune')result=await store.prune();else throw Error('Unsupported strings operation');
|
|
45
|
-
return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:Array.isArray(result)?{strings:result}:result};}
|
|
46
|
-
|
|
47
|
-
if(request.params.name==='kloudy'&&(!supplied.operation||supplied.operation==='introduce'||supplied.operation==='decline'||supplied.operation==='ask'&&isAttachIntent(supplied.goal))){const result=await greeting(supplied.operation==='decline'?'no':undefined);return {content:welcomeContent(result),structuredContent:result};}
|
|
48
|
-
const result=await useClient(async client=>{
|
|
49
|
-
const input=request.params.arguments||{};
|
|
50
|
-
if(request.params.name.startsWith('selected_'))return client.call(request.params.name.slice(9),input);
|
|
51
|
-
if(request.params.name!=='kloudy')throw new Error('Unknown tool');
|
|
2
|
+
import {instrumentToolHandler} from './activity.mjs';
|
|
3
|
+
import {configuredModels} from './models.mjs';
|
|
4
|
+
import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
|
|
5
|
+
import {welcomeContent} from './welcome.mjs';
|
|
6
|
+
import {Approvals,approvalTool,approvalResource} from './approval.mjs';
|
|
7
|
+
import {approvalHTML} from './approval-ui.mjs';
|
|
8
|
+
import {getUiCapability,RESOURCE_MIME_TYPE} from '@modelcontextprotocol/ext-apps/server';
|
|
9
|
+
import {convertPage,convertPages} from './public.mjs';
|
|
10
|
+
import {localStrings} from './strings.mjs';
|
|
11
|
+
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import {ListToolsRequestSchema,CallToolRequestSchema,ListPromptsRequestSchema,GetPromptRequestSchema,ListResourcesRequestSchema,ReadResourceRequestSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import {fileURLToPath} from 'node:url';
|
|
15
|
+
import {introduce,isAttachIntent,ideGuidance} from './introduce.mjs';
|
|
16
|
+
import {withConnection} from './connection.mjs';
|
|
17
|
+
const gateway={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. For Kloudy or /kloudy alone, use introduce to welcome this project; an attached host does not need another install question. Select one tool through the same engine. Autonomous by default; the IDE permission system controls tool calls and the engine enforces authorization.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['conversation','host_interface','models_list','action','wallet_connection','hosting','wallet','introduce','decline','convert','yield_list','query','ask','assemble','call','status','cancel','strings_list','strings_save','strings_call','strings_use','strings_keep','strings_remove','strings_prune']},conversation_operation:{type:'string',enum:['read','append','sync']},conversation:{type:'object'},action:{type:'string',enum:['chain.head','wallet.prepare','bww.fetch','bww.publish']},inputs:{type:'object'},idempotency_key:{type:'string',maxLength:128},chain:{type:'string',maxLength:100},budget_usd_micros:{type:'integer',minimum:1,maximum:1000000000000},wallet_operation:{type:'string',enum:['balance','top_up']},amount_usd:{type:'string'},goal:{type:'string'},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},url:{type:'string'},urls:{type:'array',minItems:1,maxItems:8,items:{type:'string'}},resolution:{type:'string',enum:['compact','standard']},string_name:{type:'string'},choices:{type:'array',maxItems:16,items:{type:'object',properties:{source:{type:'string'},kind:{type:'string',enum:['mcp','sdk','cli']},goal:{type:'string'}},required:['source','kind','goal'],additionalProperties:false}},selection_ids:{type:'array',items:{type:'string'},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},name:{type:'string'},arguments:{type:'object'},candidates:{type:'array',maxItems:16,items:{type:'object'}},lane:{type:'string',enum:['non_medical','medical']}},additionalProperties:false}};
|
|
18
|
+
export function createMcpServer({useClient,greet=introduce,strings=localStrings,models=configuredModels,onActivity}){
|
|
19
|
+
const server=new Server({name:'kloudy',version:'0.3.15'},{capabilities:{tools:{listChanged:true},prompts:{},resources:{}},instructions:'When the user says Kloudy or /kloudy, use operation introduce. Return the current project greeting. Do not repeatedly ask to install an already attached tool. Respect a no answer and the host permission ceiling. '+Object.values(ideGuidance).join(' ')});
|
|
20
|
+
const approvals=new Approvals(),hasUI=()=>getUiCapability(server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE);
|
|
21
|
+
server.setRequestHandler(ListResourcesRequestSchema,async()=>({resources:hasUI()?[{uri:approvalResource,name:'Kloudy action approval',mimeType:RESOURCE_MIME_TYPE}]:[]}));
|
|
22
|
+
server.setRequestHandler(ReadResourceRequestSchema,async request=>{if(!hasUI()||request.params.uri!==approvalResource)throw Error('Unsupported UI resource');return {contents:[{uri:approvalResource,mimeType:RESOURCE_MIME_TYPE,text:approvalHTML,_meta:{ui:{csp:{connectDomains:[],resourceDomains:[]}}}}]};});
|
|
23
|
+
async function projectRoot(){try{const roots=await server.listRoots();if(roots.roots?.[0]?.uri?.startsWith('file:'))return fileURLToPath(roots.roots[0].uri);}catch{}return process.env.CLAUDE_PROJECT_DIR||process.cwd();}
|
|
24
|
+
async function greeting(decision){let project=process.env.CLAUDE_PROJECT_DIR||process.cwd();try{const roots=await server.listRoots();if(roots.roots?.[0]?.uri?.startsWith('file:'))project=fileURLToPath(roots.roots[0].uri);}catch{}return greet({project,decision,hostAttached:true});}
|
|
25
|
+
server.setRequestHandler(ListPromptsRequestSchema,async()=>({prompts:[{name:'kloudy',description:'Welcome the current project to Kloudy and explain how to use it.'}]}));
|
|
26
|
+
server.setRequestHandler(GetPromptRequestSchema,async request=>{if(request.params.name!=='kloudy')throw Error('Unknown prompt');const result=await greeting();return {description:'Ask for Kloudy',messages:[{role:'assistant',content:welcomeContent(result)[0]}]};});
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema,async()=>{
|
|
28
|
+
let selected;try{selected=await useClient(client=>client.state.selection?.tool);}catch{}
|
|
29
|
+
const meta=hasUI()?{_meta:{ui:{resourceUri:approvalResource}}}:{};return {tools:[{...gateway,...meta},...(selected?[{...selected,name:'selected_'+selected.name,...meta}]:[]),...(hasUI()?[approvalTool]:[])]};
|
|
30
|
+
});
|
|
31
|
+
server.setRequestHandler(CallToolRequestSchema,instrumentToolHandler(async request=>{
|
|
32
|
+
try{
|
|
33
|
+
const supplied=request.params.arguments||{};
|
|
34
|
+
if(request.params.name==='kloudy'&&supplied.operation==='models_list'){if(Object.keys(supplied).length!==1)throw Error('Model discovery uses host configuration only');const result=await models().list();return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
35
|
+
|
|
36
|
+
if(request.params.name==='kloudy'&&['convert','yield_list'].includes(supplied.operation)){const result=await (supplied.operation==='convert'?convertPage(supplied.url,{resolution:supplied.resolution}):convertPages(supplied.urls,{resolution:supplied.resolution}));return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
37
|
+
if(request.params.name==='kloudy_approval'){if(!hasUI())throw Error('This host has no inline approval UI');const result=await approvals.decide(supplied,useClient);return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
|
|
38
|
+
if(request.params.name==='kloudy'&&supplied.operation?.startsWith('strings_')){const store=strings({project:await projectRoot(),mode:'auto'}),op=supplied.operation.slice(8);let result;
|
|
39
|
+
if(op==='save')result=await store.save(supplied.string_name,supplied.choices);
|
|
40
|
+
else if(op==='call'){result=await store.call(supplied.string_name,supplied.name,supplied.arguments,{selectionId:supplied.selection_id});if(result?.state==='awaiting_approval'){const saved=(await store.list()).find(s=>s.name===supplied.string_name),route=saved?.activeRoutes?.find(r=>r.name===supplied.name&&(!supplied.selection_id||r.selection_id===supplied.selection_id));if(!route)throw Error('Selected route unavailable');return approvals.present(result,route.selection_id,work=>store.connect(route.source,route.kind,work));}}
|
|
41
|
+
else if(op==='list')result=await store.list();
|
|
42
|
+
else if(op==='use'){result=await store.use(supplied.string_name);}
|
|
43
|
+
else if(['keep','remove'].includes(op))result=await store[op](supplied.string_name);
|
|
44
|
+
else if(op==='prune')result=await store.prune();else throw Error('Unsupported strings operation');
|
|
45
|
+
return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:Array.isArray(result)?{strings:result}:result};}
|
|
46
|
+
|
|
47
|
+
if(request.params.name==='kloudy'&&(!supplied.operation||supplied.operation==='introduce'||supplied.operation==='decline'||supplied.operation==='ask'&&isAttachIntent(supplied.goal))){const result=await greeting(supplied.operation==='decline'?'no':undefined);return {content:welcomeContent(result),structuredContent:result};}
|
|
48
|
+
const result=await useClient(async client=>{
|
|
49
|
+
const input=request.params.arguments||{};
|
|
50
|
+
if(request.params.name.startsWith('selected_'))return client.call(request.params.name.slice(9),input);
|
|
51
|
+
if(request.params.name!=='kloudy')throw new Error('Unknown tool');
|
|
52
52
|
if(input.operation==='conversation'){if(!client.conversation)throw Error('Conversation sync requires a scoped HTTPK connection');return client.conversation(input.conversation_operation,input.conversation);}
|
|
53
|
-
if(input.operation==='host_interface')return client.negotiateHost();
|
|
54
|
-
if(input.operation==='action')return client.action(input.action,input.inputs,{idempotencyKey:input.idempotency_key});
|
|
55
|
-
if(input.operation==='wallet_connection')return client.walletConnection(input.chain);
|
|
56
|
-
if(input.operation==='hosting')return client.hosting({budgetUsdMicros:input.budget_usd_micros});
|
|
57
|
-
if(input.operation==='wallet'){if(!client.wallet)throw Error('Wallet previews require HTTPK');return client.wallet({operation:input.wallet_operation||'balance',amount_usd:input.amount_usd});}
|
|
58
|
-
if(input.operation==='query'){if(!client.query)throw Error('Query needs an HTTPK connection');return client.query(input.goal,{scope:input.scope,limit:input.limit});}
|
|
59
|
-
if(input.operation==='ask')return client.ask(input.goal,{candidates:input.candidates||[],lane:input.lane||'non_medical'});
|
|
60
|
-
if(input.operation==='assemble'){if(!client.assemble)throw Error('Assembly requires an HTTPK connection');return client.assemble(input.selection_ids);}
|
|
61
|
-
if(input.operation==='call'){if(!client.assemble||!input.selection_id)throw Error('Exact HTTPK selection required');return client.call(input.name,input.arguments,{selectionId:input.selection_id});}
|
|
62
|
-
if(input.operation==='status')return client.status();
|
|
63
|
-
if(input.operation==='cancel')return client.decide('cancel');
|
|
64
|
-
throw new Error('Unsupported operation');
|
|
65
|
-
});
|
|
66
|
-
// Prompt the host to fetch the newly selected singular tool, not a server dump.
|
|
67
|
-
server.sendToolListChanged().catch(()=>{});
|
|
68
|
-
if(result?.state==='awaiting_approval')return approvals.present(result,result.selection_id);
|
|
69
|
-
return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};
|
|
70
|
-
}catch(error){return {isError:true,content:[{type:'text',text:JSON.stringify({error:error.code||'request_failed',message:error.code?error.message:'The tool request could not be completed.'})}]};}
|
|
71
|
-
},onActivity));
|
|
72
|
-
return server;
|
|
73
|
-
}
|
|
74
|
-
export async function serveMcp(connection){const server=createMcpServer({useClient:work=>withConnection(connection,work,{mode:'auto'}),greet:options=>introduce({...options,connection})});await server.connect(new StdioServerTransport());return server;}
|
|
53
|
+
if(input.operation==='host_interface')return client.negotiateHost();
|
|
54
|
+
if(input.operation==='action')return client.action(input.action,input.inputs,{idempotencyKey:input.idempotency_key});
|
|
55
|
+
if(input.operation==='wallet_connection')return client.walletConnection(input.chain);
|
|
56
|
+
if(input.operation==='hosting')return client.hosting({budgetUsdMicros:input.budget_usd_micros});
|
|
57
|
+
if(input.operation==='wallet'){if(!client.wallet)throw Error('Wallet previews require HTTPK');return client.wallet({operation:input.wallet_operation||'balance',amount_usd:input.amount_usd});}
|
|
58
|
+
if(input.operation==='query'){if(!client.query)throw Error('Query needs an HTTPK connection');return client.query(input.goal,{scope:input.scope,limit:input.limit});}
|
|
59
|
+
if(input.operation==='ask')return client.ask(input.goal,{candidates:input.candidates||[],lane:input.lane||'non_medical'});
|
|
60
|
+
if(input.operation==='assemble'){if(!client.assemble)throw Error('Assembly requires an HTTPK connection');return client.assemble(input.selection_ids);}
|
|
61
|
+
if(input.operation==='call'){if(!client.assemble||!input.selection_id)throw Error('Exact HTTPK selection required');return client.call(input.name,input.arguments,{selectionId:input.selection_id});}
|
|
62
|
+
if(input.operation==='status')return client.status();
|
|
63
|
+
if(input.operation==='cancel')return client.decide('cancel');
|
|
64
|
+
throw new Error('Unsupported operation');
|
|
65
|
+
});
|
|
66
|
+
// Prompt the host to fetch the newly selected singular tool, not a server dump.
|
|
67
|
+
server.sendToolListChanged().catch(()=>{});
|
|
68
|
+
if(result?.state==='awaiting_approval')return approvals.present(result,result.selection_id);
|
|
69
|
+
return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};
|
|
70
|
+
}catch(error){return {isError:true,content:[{type:'text',text:JSON.stringify({error:error.code||'request_failed',message:error.code?error.message:'The tool request could not be completed.'})}]};}
|
|
71
|
+
},onActivity));
|
|
72
|
+
return server;
|
|
73
|
+
}
|
|
74
|
+
export async function serveMcp(connection){const server=createMcpServer({useClient:work=>withConnection(connection,work,{mode:'auto'}),greet:options=>introduce({...options,connection})});await server.connect(new StdioServerTransport());return server;}
|
package/src/models.mjs
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
// User-owned compute adapter. No provider provisioning, routing or vault lives here.
|
|
2
|
-
import {KloudyError} from './client.mjs';
|
|
3
|
-
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
4
|
-
const modelName=value=>typeof value==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]{0,199}$/.test(value);
|
|
5
|
-
export class ModelClient {
|
|
6
|
-
constructor({endpoint='http://127.0.0.1:11434/v1',resolveKey=async()=>null,fetcher=fetch,timeoutMs=90000}={}){
|
|
7
|
-
let url;try{url=new URL(endpoint);}catch{}
|
|
8
|
-
const local=url&&['127.0.0.1','[::1]','localhost'].includes(url.hostname);
|
|
9
|
-
if(!url||url.username||url.password||url.search||url.hash||!(url.protocol==='https:'||url.protocol==='http:'&&local))fail('invalid_model_endpoint','Use your HTTPS model endpoint or local Ollama endpoint without credentials in the URL.');
|
|
10
|
-
if(typeof resolveKey!=='function'||!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>120000)fail('invalid_model_config','Invalid model client configuration.');
|
|
11
|
-
this.endpoint=url.href.replace(/\/$/,'');this.local=local;this.ollama=local&&url.port==='11434'&&url.pathname.replace(/\/$/,'')==='/v1';this.resolveKey=resolveKey;this.fetcher=fetcher;this.timeoutMs=timeoutMs;
|
|
12
|
-
}
|
|
13
|
-
async request(path,body){
|
|
14
|
-
let key;try{key=await this.resolveKey();}catch{fail('model_key_unavailable','The host key resolver is unavailable.');}
|
|
15
|
-
if(key!=null&&(typeof key!=='string'||!key||key.length>8192||/[\r\n]/.test(key)))fail('model_key_unavailable','The configured model credential is unavailable.');
|
|
16
|
-
if(!this.local&&!key)fail('model_key_unavailable','Connect your own gateway credential through your host’s protected key resolver.');
|
|
17
|
-
try{
|
|
18
|
-
const target=path==='/api/tags'&&this.ollama?new URL('/api/tags',this.endpoint).href:this.endpoint+path;
|
|
19
|
-
const response=await this.fetcher(target,{method:body?'POST':'GET',redirect:'error',signal:AbortSignal.timeout(this.timeoutMs),headers:{accept:'application/json',...(key?{authorization:'Bearer '+key}:{}),...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
|
|
20
|
-
if(!response.ok)fail(response.status===401||response.status===403?'model_authorization_required':'model_unavailable','Your model endpoint rejected the request. Nothing was retried.');
|
|
21
|
-
if(!response.headers.get('content-type')?.includes('application/json'))fail('invalid_model_response','Expected a JSON model response.');
|
|
22
|
-
const reader=response.body.getReader();let size=0,chunks=[];
|
|
23
|
-
try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>1048576)fail('model_response_limit','Model response exceeds the limit.');chunks.push(value);}}finally{await reader.cancel().catch(()=>{});}
|
|
24
|
-
const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
|
|
25
|
-
let result;try{result=JSON.parse(new TextDecoder().decode(bytes));}catch{fail('invalid_model_response','Expected a valid JSON model response.');}
|
|
26
|
-
// An upstream error/body must not echo a gateway secret into a tool result.
|
|
27
|
-
if(key&&JSON.stringify(result).includes(key))fail('invalid_model_response','Model response contained protected connection data.');
|
|
28
|
-
return result;
|
|
29
|
-
}catch(error){if(error instanceof KloudyError)throw error;fail('model_unavailable','Your model endpoint is unavailable. Nothing was retried.');}
|
|
30
|
-
}
|
|
31
|
-
async list(){
|
|
32
|
-
const response=await this.request('/models');
|
|
33
|
-
if(!Array.isArray(response?.data)||response.data.length>512||response.data.some(x=>!modelName(x?.id)))fail('invalid_model_catalog','The model endpoint returned an invalid catalog.');
|
|
34
|
-
return {schema:'kloudy.models/1',state:'reachable',compute:this.local?'local_endpoint':'user_provider',provider_bill:'user',inference_verified:false,models:[...new Set(response.data.map(x=>x.id))].slice(0,128).map(id=>({id,status:'advertised'})),truncated:new Set(response.data.map(x=>x.id)).size>128};
|
|
35
|
-
}
|
|
36
|
-
async complete({model,prompt,maxTokens=128,allowProviderCharge=false}={}){
|
|
37
|
-
if(!modelName(model)||typeof prompt!=='string'||!prompt.trim()||prompt.length>16000||!Number.isInteger(maxTokens)||maxTokens<1||maxTokens>4096||typeof allowProviderCharge!=='boolean')fail('invalid_model_request','Supply a model, bounded prompt and token limit.');
|
|
38
|
-
if(!this.ollama&&!allowProviderCharge)fail('provider_charge_approval_required','Explicit authorization is required before using a provider through this endpoint.');
|
|
39
|
-
const catalog=await this.list();if(!catalog.models.some(x=>x.id===model))fail('model_not_available','That model is not advertised by your endpoint.');
|
|
40
|
-
if(this.ollama&&!allowProviderCharge){
|
|
41
|
-
const tags=await this.request('/api/tags');
|
|
42
|
-
const installed=tags?.models?.find(x=>x.name===model||x.model===model);
|
|
43
|
-
if(!installed||!Number.isSafeInteger(installed.size)||installed.size<=0||installed.remote_host||installed.remote_model||/:cloud(?:$|[-:])/.test(model))fail('provider_charge_approval_required','This model is not verified as locally installed. Review its provider cost before running it.');
|
|
44
|
-
}
|
|
45
|
-
const result=await this.request('/chat/completions',{model,messages:[{role:'user',content:prompt}],max_tokens:maxTokens,stream:false});
|
|
46
|
-
const text=result?.choices?.[0]?.message?.content;
|
|
47
|
-
if(typeof text!=='string'||!text.trim()||text.length>32000)fail('invalid_model_response','The model did not return a bounded text answer.');
|
|
48
|
-
return {schema:'kloudy.model-result/1',model,text,compute:catalog.compute,provider_bill:'user',usage:result.usage&&['prompt_tokens','completion_tokens','total_tokens'].every(k=>Number.isSafeInteger(result.usage[k])&&result.usage[k]>=0)?Object.fromEntries(['prompt_tokens','completion_tokens','total_tokens'].map(k=>[k,result.usage[k]])):null};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export function configuredModels(env=process.env){
|
|
52
|
-
const name=env.KLOUDY_MODEL_KEY_ENV;
|
|
53
|
-
if(name&&!/^[A-Z][A-Z0-9_]{0,99}$/.test(name))fail('invalid_model_config','Use a credential environment reference, never the key itself.');
|
|
54
|
-
return new ModelClient({endpoint:env.KLOUDY_MODEL_ENDPOINT||undefined,resolveKey:async()=>name?env[name]??null:null});
|
|
55
|
-
}
|
|
1
|
+
// User-owned compute adapter. No provider provisioning, routing or vault lives here.
|
|
2
|
+
import {KloudyError} from './client.mjs';
|
|
3
|
+
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
4
|
+
const modelName=value=>typeof value==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]{0,199}$/.test(value);
|
|
5
|
+
export class ModelClient {
|
|
6
|
+
constructor({endpoint='http://127.0.0.1:11434/v1',resolveKey=async()=>null,fetcher=fetch,timeoutMs=90000}={}){
|
|
7
|
+
let url;try{url=new URL(endpoint);}catch{}
|
|
8
|
+
const local=url&&['127.0.0.1','[::1]','localhost'].includes(url.hostname);
|
|
9
|
+
if(!url||url.username||url.password||url.search||url.hash||!(url.protocol==='https:'||url.protocol==='http:'&&local))fail('invalid_model_endpoint','Use your HTTPS model endpoint or local Ollama endpoint without credentials in the URL.');
|
|
10
|
+
if(typeof resolveKey!=='function'||!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>120000)fail('invalid_model_config','Invalid model client configuration.');
|
|
11
|
+
this.endpoint=url.href.replace(/\/$/,'');this.local=local;this.ollama=local&&url.port==='11434'&&url.pathname.replace(/\/$/,'')==='/v1';this.resolveKey=resolveKey;this.fetcher=fetcher;this.timeoutMs=timeoutMs;
|
|
12
|
+
}
|
|
13
|
+
async request(path,body){
|
|
14
|
+
let key;try{key=await this.resolveKey();}catch{fail('model_key_unavailable','The host key resolver is unavailable.');}
|
|
15
|
+
if(key!=null&&(typeof key!=='string'||!key||key.length>8192||/[\r\n]/.test(key)))fail('model_key_unavailable','The configured model credential is unavailable.');
|
|
16
|
+
if(!this.local&&!key)fail('model_key_unavailable','Connect your own gateway credential through your host’s protected key resolver.');
|
|
17
|
+
try{
|
|
18
|
+
const target=path==='/api/tags'&&this.ollama?new URL('/api/tags',this.endpoint).href:this.endpoint+path;
|
|
19
|
+
const response=await this.fetcher(target,{method:body?'POST':'GET',redirect:'error',signal:AbortSignal.timeout(this.timeoutMs),headers:{accept:'application/json',...(key?{authorization:'Bearer '+key}:{}),...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
|
|
20
|
+
if(!response.ok)fail(response.status===401||response.status===403?'model_authorization_required':'model_unavailable','Your model endpoint rejected the request. Nothing was retried.');
|
|
21
|
+
if(!response.headers.get('content-type')?.includes('application/json'))fail('invalid_model_response','Expected a JSON model response.');
|
|
22
|
+
const reader=response.body.getReader();let size=0,chunks=[];
|
|
23
|
+
try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>1048576)fail('model_response_limit','Model response exceeds the limit.');chunks.push(value);}}finally{await reader.cancel().catch(()=>{});}
|
|
24
|
+
const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
|
|
25
|
+
let result;try{result=JSON.parse(new TextDecoder().decode(bytes));}catch{fail('invalid_model_response','Expected a valid JSON model response.');}
|
|
26
|
+
// An upstream error/body must not echo a gateway secret into a tool result.
|
|
27
|
+
if(key&&JSON.stringify(result).includes(key))fail('invalid_model_response','Model response contained protected connection data.');
|
|
28
|
+
return result;
|
|
29
|
+
}catch(error){if(error instanceof KloudyError)throw error;fail('model_unavailable','Your model endpoint is unavailable. Nothing was retried.');}
|
|
30
|
+
}
|
|
31
|
+
async list(){
|
|
32
|
+
const response=await this.request('/models');
|
|
33
|
+
if(!Array.isArray(response?.data)||response.data.length>512||response.data.some(x=>!modelName(x?.id)))fail('invalid_model_catalog','The model endpoint returned an invalid catalog.');
|
|
34
|
+
return {schema:'kloudy.models/1',state:'reachable',compute:this.local?'local_endpoint':'user_provider',provider_bill:'user',inference_verified:false,models:[...new Set(response.data.map(x=>x.id))].slice(0,128).map(id=>({id,status:'advertised'})),truncated:new Set(response.data.map(x=>x.id)).size>128};
|
|
35
|
+
}
|
|
36
|
+
async complete({model,prompt,maxTokens=128,allowProviderCharge=false}={}){
|
|
37
|
+
if(!modelName(model)||typeof prompt!=='string'||!prompt.trim()||prompt.length>16000||!Number.isInteger(maxTokens)||maxTokens<1||maxTokens>4096||typeof allowProviderCharge!=='boolean')fail('invalid_model_request','Supply a model, bounded prompt and token limit.');
|
|
38
|
+
if(!this.ollama&&!allowProviderCharge)fail('provider_charge_approval_required','Explicit authorization is required before using a provider through this endpoint.');
|
|
39
|
+
const catalog=await this.list();if(!catalog.models.some(x=>x.id===model))fail('model_not_available','That model is not advertised by your endpoint.');
|
|
40
|
+
if(this.ollama&&!allowProviderCharge){
|
|
41
|
+
const tags=await this.request('/api/tags');
|
|
42
|
+
const installed=tags?.models?.find(x=>x.name===model||x.model===model);
|
|
43
|
+
if(!installed||!Number.isSafeInteger(installed.size)||installed.size<=0||installed.remote_host||installed.remote_model||/:cloud(?:$|[-:])/.test(model))fail('provider_charge_approval_required','This model is not verified as locally installed. Review its provider cost before running it.');
|
|
44
|
+
}
|
|
45
|
+
const result=await this.request('/chat/completions',{model,messages:[{role:'user',content:prompt}],max_tokens:maxTokens,stream:false});
|
|
46
|
+
const text=result?.choices?.[0]?.message?.content;
|
|
47
|
+
if(typeof text!=='string'||!text.trim()||text.length>32000)fail('invalid_model_response','The model did not return a bounded text answer.');
|
|
48
|
+
return {schema:'kloudy.model-result/1',model,text,compute:catalog.compute,provider_bill:'user',usage:result.usage&&['prompt_tokens','completion_tokens','total_tokens'].every(k=>Number.isSafeInteger(result.usage[k])&&result.usage[k]>=0)?Object.fromEntries(['prompt_tokens','completion_tokens','total_tokens'].map(k=>[k,result.usage[k]])):null};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function configuredModels(env=process.env){
|
|
52
|
+
const name=env.KLOUDY_MODEL_KEY_ENV;
|
|
53
|
+
if(name&&!/^[A-Z][A-Z0-9_]{0,99}$/.test(name))fail('invalid_model_config','Use a credential environment reference, never the key itself.');
|
|
54
|
+
return new ModelClient({endpoint:env.KLOUDY_MODEL_ENDPOINT||undefined,resolveKey:async()=>name?env[name]??null:null});
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {preferencesHTML} from './preferences-ui.mjs';
|
|
2
|
+
export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
|
|
3
|
+
export const sampleTool={
|
|
4
|
+
name:'show_tool_preferences_card',title:'Tool preferences card',
|
|
5
|
+
description:'Show three short questions about preferred tool types, where projects run, and how tool updates appear. Use when the user wants to set or change these preferences. Displays choices only; does not install or run tools. Show once, not for every tool.',
|
|
6
|
+
annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},
|
|
7
|
+
inputSchema:{type:'object',properties:{
|
|
8
|
+
kind:{type:'string',enum:['preferences'],description:'Show the three tool-preference questions.'},
|
|
9
|
+
surface:{type:'string',enum:['ide','chatbot','site'],default:'chatbot',description:'Where the card appears: an editor, a chatbot, or the Kloudy website. Changes wording only.'}
|
|
10
|
+
},required:['kind'],additionalProperties:false},
|
|
11
|
+
_meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}
|
|
12
|
+
};
|
|
13
|
+
export const sampleResource={uri:SAMPLE_URI,name:'Tool preferences card',description:'Three questions about tools, project location and updates.',mimeType:'text/html;profile=mcp-app'};
|
|
14
|
+
export function sampleArguments(a){return !!a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&a.kind==='preferences'&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
|
|
15
|
+
export function sampleMessage(method,params){return !!params&&typeof params==='object'&&!Array.isArray(params)&&(method==='resources/read'&¶ms.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&['show_tool_preferences_card','kloudy_sample_card'].includes(params.name)&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments));}
|
|
16
|
+
export function sampleResult(kind='preferences',surface='chatbot'){
|
|
17
|
+
if(!sampleArguments({kind,surface}))throw Error('Choose tool preferences.');
|
|
18
|
+
const url='https://kloudy.ai/samples?frontdoor='+surface;
|
|
19
|
+
return {content:[{type:'text',text:`Tool preferences card\n\n[Choose your tools, project location and updates](${url})\n\nThese choices do not install or run tools.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
|
|
20
|
+
}
|
|
21
|
+
export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:preferencesHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Three short questions about tool preferences.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}
|