@@ -2,6 +2,8 @@ import { createInterface } from "node:readline";
22import { existsSync , readdirSync , readFileSync } from "node:fs" ;
33import { dirname , join } from "node:path" ;
44import { fileURLToPath } from "node:url" ;
5+ import { getToken } from "./auth" ;
6+ import { PlatformClient } from "./client" ;
57
68// --- Types ---
79
@@ -56,12 +58,24 @@ function resolveDocsDir(): string {
5658 throw new Error ( `Void docs directory not found. Expected bundled docs at: ${ bundledDocsDir } ` ) ;
5759}
5860
61+ function findMdFiles ( dir : string , prefix = "" ) : string [ ] {
62+ const results : string [ ] = [ ] ;
63+ for ( const entry of readdirSync ( dir , { withFileTypes : true } ) ) {
64+ if ( entry . name === "node_modules" || entry . name . startsWith ( "." ) ) continue ;
65+ const rel = prefix ? prefix + "/" + entry . name : entry . name ;
66+ if ( entry . isDirectory ( ) ) {
67+ results . push ( ...findMdFiles ( join ( dir , entry . name ) , rel ) ) ;
68+ } else if ( entry . name . endsWith ( ".md" ) ) {
69+ results . push ( rel ) ;
70+ }
71+ }
72+ return results ;
73+ }
74+
5975function loadDocs ( ) : DocPage [ ] {
6076 const docsDir = resolveDocsDir ( ) ;
6177
62- const files = readdirSync ( docsDir , { recursive : true } )
63- . filter ( ( f ) : f is string => typeof f === "string" && f . endsWith ( ".md" ) && ! f . startsWith ( "." ) )
64- . sort ( ) ;
78+ const files = findMdFiles ( docsDir ) . sort ( ) ;
6579
6680 return files . map ( ( file ) => {
6781 let content = readFileSync ( join ( docsDir , file ) , "utf-8" ) ;
@@ -136,6 +150,23 @@ export function searchDocs(pages: DocPage[], query: string): SearchResult[] {
136150 return scored . slice ( 0 , 5 ) ;
137151}
138152
153+ // --- Query SDK ---
154+
155+ function createQuerySdk ( token : string ) {
156+ const client = new PlatformClient ( token ) ;
157+ return {
158+ me : ( ) => client . whoami ( ) ,
159+ projects : ( ) => client . listProjects ( ) ,
160+ deployments : ( projectId : string ) => client . listDeployments ( projectId ) ,
161+ logs : ( projectId : string , opts ?: { since ?: string ; limit ?: number } ) =>
162+ client . getLogs ( projectId , opts ?. since ?? "1h" , opts ?. limit ?? 200 ) ,
163+ secrets : ( projectId : string ) => client . listSecrets ( projectId ) ,
164+ domains : ( projectId : string ) => client . listDomains ( projectId ) ,
165+ domainStatus : ( projectId : string , domainId : string ) =>
166+ client . getDomainStatus ( projectId , domainId ) ,
167+ } ;
168+ }
169+
139170// --- Tool definitions ---
140171
141172const TOOLS = [
@@ -173,11 +204,46 @@ const TOOLS = [
173204 required : [ "query" ] ,
174205 } ,
175206 } ,
207+ {
208+ name : "query" ,
209+ description : `Execute JavaScript to query the Void platform API.
210+
211+ Available methods on the "void_" object:
212+ void_.me() → { login: string, plan: string }
213+ void_.projects() → { id, slug, type, created_at, custom_domain }[]
214+ void_.deployments(projectId) → { id, status, source, commit, created_at, deployed_at }[]
215+ void_.logs(projectId, opts?) → { timestamp, outcome, logs, exceptions, url, method, status }[]
216+ opts: { since?: string (default "1h"), limit?: number (default 200) }
217+ void_.secrets(projectId) → { name }[]
218+ void_.domains(projectId) → { id, hostname, status, cname_target }[]
219+ void_.domainStatus(projectId, domainId) → { hostname, status, ssl_status, verification_errors }
220+
221+ Write an async arrow function in JavaScript. The SDK is available as "void_".
222+ Example: async () => { const ps = await void_.projects(); return ps.map(p => p.slug); }` ,
223+ inputSchema : {
224+ type : "object" as const ,
225+ properties : {
226+ code : {
227+ type : "string" ,
228+ description : "JavaScript async arrow function to execute" ,
229+ } ,
230+ } ,
231+ required : [ "code" ] ,
232+ } ,
233+ } ,
176234] ;
177235
178236// --- JSON-RPC dispatch ---
179237
180- function handleRequest ( pages : DocPage [ ] , req : JsonRpcRequest ) : object | null {
238+ export interface McpContext {
239+ pages : DocPage [ ] ;
240+ token : string | null ;
241+ }
242+
243+ export function handleRequest (
244+ ctx : McpContext ,
245+ req : JsonRpcRequest ,
246+ ) : object | null | Promise < object | null > {
181247 const { method, id, params } = req ;
182248
183249 if ( method === "initialize" ) {
@@ -213,7 +279,7 @@ function handleRequest(pages: DocPage[], req: JsonRpcRequest): object | null {
213279 > ;
214280
215281 if ( toolName === "list_pages" ) {
216- const list = pages . map ( ( p ) => ( { slug : p . slug , title : p . title } ) ) ;
282+ const list = ctx . pages . map ( ( p ) => ( { slug : p . slug , title : p . title } ) ) ;
217283 return {
218284 jsonrpc : "2.0" ,
219285 id,
@@ -223,7 +289,7 @@ function handleRequest(pages: DocPage[], req: JsonRpcRequest): object | null {
223289
224290 if ( toolName === "get_page" ) {
225291 const slug = toolArgs . slug as string ;
226- const page = pages . find ( ( p ) => p . slug === slug ) ;
292+ const page = ctx . pages . find ( ( p ) => p . slug === slug ) ;
227293 if ( ! page ) {
228294 return {
229295 jsonrpc : "2.0" ,
@@ -240,14 +306,67 @@ function handleRequest(pages: DocPage[], req: JsonRpcRequest): object | null {
240306
241307 if ( toolName === "search_docs" ) {
242308 const query = toolArgs . query as string ;
243- const results = searchDocs ( pages , query ) ;
309+ const results = searchDocs ( ctx . pages , query ) ;
244310 return {
245311 jsonrpc : "2.0" ,
246312 id,
247313 result : { content : [ { type : "text" , text : JSON . stringify ( results , null , 2 ) } ] } ,
248314 } ;
249315 }
250316
317+ if ( toolName === "query" ) {
318+ const code = toolArgs . code as string ;
319+
320+ if ( ! ctx . token ) {
321+ return {
322+ jsonrpc : "2.0" ,
323+ id,
324+ result : {
325+ content : [ { type : "text" , text : "Not logged in. Run `void auth login` first." } ] ,
326+ isError : true ,
327+ } ,
328+ } ;
329+ }
330+
331+ const sdk = createQuerySdk ( ctx . token ) ;
332+ const AsyncFunction = async function ( ) { } . constructor as new (
333+ ...args : string [ ]
334+ ) => ( ...args : unknown [ ] ) => Promise < unknown > ;
335+
336+ return ( async ( ) => {
337+ try {
338+ const fn = new AsyncFunction ( "void_" , `return await (${ code } )()` ) ;
339+ const result = await Promise . race ( [
340+ fn ( sdk ) ,
341+ new Promise ( ( _ , reject ) =>
342+ setTimeout ( ( ) => reject ( new Error ( "Query timed out after 10 seconds" ) ) , 10_000 ) ,
343+ ) ,
344+ ] ) ;
345+ return {
346+ jsonrpc : "2.0" ,
347+ id,
348+ result : {
349+ content : [ { type : "text" , text : JSON . stringify ( result , null , 2 ) } ] ,
350+ } ,
351+ } ;
352+ } catch ( err ) {
353+ return {
354+ jsonrpc : "2.0" ,
355+ id,
356+ result : {
357+ content : [
358+ {
359+ type : "text" ,
360+ text : `Error: ${ err instanceof Error ? err . message : String ( err ) } ` ,
361+ } ,
362+ ] ,
363+ isError : true ,
364+ } ,
365+ } ;
366+ }
367+ } ) ( ) ;
368+ }
369+
251370 return {
252371 jsonrpc : "2.0" ,
253372 id,
@@ -266,10 +385,12 @@ function handleRequest(pages: DocPage[], req: JsonRpcRequest): object | null {
266385
267386export function runMcp ( ) : void {
268387 const pages = loadDocs ( ) ;
388+ const token = getToken ( ) ;
389+ const ctx : McpContext = { pages, token } ;
269390
270391 const rl = createInterface ( { input : process . stdin } ) ;
271392
272- rl . on ( "line" , ( line ) => {
393+ rl . on ( "line" , async ( line ) => {
273394 let req : JsonRpcRequest ;
274395 try {
275396 req = JSON . parse ( line ) ;
@@ -284,7 +405,7 @@ export function runMcp(): void {
284405 return ;
285406 }
286407
287- const response = handleRequest ( pages , req ) ;
408+ const response = await handleRequest ( ctx , req ) ;
288409 if ( response !== null ) {
289410 process . stdout . write ( JSON . stringify ( response ) + "\n" ) ;
290411 }
0 commit comments