Skip to main content
Glama

pursIdeLoad

Load PureScript modules into the IDE server to enable type checking and code completions. Essential first step after starting the IDE server for proper functionality of other development tools.

Instructions

Load PureScript modules into the IDE server for type checking and completions. PREREQUISITE: IDE server must be running. ALWAYS run this first after starting the IDE server before using other pursIde* tools.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
modulesNoOptional: specific modules to load. If omitted, attempts to load all compiled modules.

Implementation Reference

  • Handler function for the 'pursIdeLoad' tool. Forwards the request to the purs ide server by calling sendCommandToPursIde with command 'load' and the provided params (modules array or empty). Returns the MCP-formatted response with the result.
    "pursIdeLoad": async (args) => {
        const params = args || {}; // If args is null/undefined, pass empty object for default load all
        const result = await sendCommandToPursIde({ command: "load", params });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
    },
  • Input schema definition for the 'pursIdeLoad' tool, defining optional 'modules' array parameter. Returned by tools/list endpoint.
    {
        name: "pursIdeLoad",
        description: "Load PureScript modules into the IDE server for type checking and completions. PREREQUISITE: IDE server must be running. ALWAYS run this first after starting the IDE server before using other pursIde* tools.",
        inputSchema: {
            type: "object",
            properties: {
                modules: { 
                    type: "array", 
                    items: { type: "string" },
                    description: "Optional: specific modules to load. If omitted, attempts to load all compiled modules."
                }
            },
            additionalProperties: false
        }
    },
  • Core helper function used by pursIdeLoad (and other pursIde* tools) to send JSON commands to the running purs ide TCP server and receive/parse responses.
    function sendCommandToPursIde(commandPayload) {
        return new Promise((resolve, reject) => {
            if (!pursIdeProcess || !pursIdeIsReady || !pursIdeServerPort) {
                return reject(new Error("purs ide server is not running or not ready."));
            }
            const client = new net.Socket();
            let responseData = '';
            client.connect(pursIdeServerPort, '127.0.0.1', () => {
                logToStderr(`[MCP Client->purs ide]: Sending command: ${JSON.stringify(commandPayload).substring(0,100)}...`, 'debug');
                client.write(JSON.stringify(commandPayload) + '\n');
            });
            client.on('data', (data) => {
                responseData += data.toString();
                if (responseData.includes('\n')) {
                     const completeResponses = responseData.split('\n').filter(Boolean);
                     responseData = ''; 
                     if (completeResponses.length > 0) {
                        try {
                            resolve(JSON.parse(completeResponses[0].trim()));
                        } catch (e) {
                            reject(new Error(`Failed to parse JSON response from purs ide: ${e.message}. Raw: ${completeResponses[0]}`));
                        }
                     }
                     client.end(); 
                }
            });
            client.on('end', () => {
                if (responseData.trim()) {
                     try { resolve(JSON.parse(responseData.trim())); } 
                     catch (e) { reject(new Error(`Failed to parse JSON response from purs ide on end: ${e.message}. Raw: ${responseData}`));}
                }
            });
            client.on('close', () => { logToStderr(`[MCP Client->purs ide]: Connection closed.`, 'debug'); });
            client.on('error', (err) => reject(new Error(`TCP connection error with purs ide server: ${err.message}`)));
        });
    }
  • index.js:791-1127 (registration)
    INTERNAL_TOOL_HANDLERS object maps tool names to their async handler functions. Used in handleMcpRequest for 'tools/call' dispatch to route 'pursIdeLoad' calls to its handler.
    const INTERNAL_TOOL_HANDLERS = {
        "get_server_status": internalHandleGetServerStatus,
        "echo": internalHandleEcho,
        "query_purescript_ast": internalHandleQueryPurescriptAst,
        "start_purs_ide_server": internalHandleStartPursIdeServer,
        "stop_purs_ide_server": internalHandleStopPursIdeServer,
        "query_purs_ide": internalHandleQueryPursIde,
        "generate_dependency_graph": internalHandleGenerateDependencyGraph,
        // --- Phase 1: Core AST Query Tool Handlers (to be added) ---
        "getModuleName": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, true);
            const tree = purescriptTsParser.parse(code);
            // Corrected query to capture the full text of the qualified_module node
            const query = new Query(PureScriptLanguage, `(purescript name: (qualified_module) @module.qname)`);
            const captures = query.captures(tree.rootNode);
            if (captures.length > 0 && captures[0].name === 'module.qname') {
                // The text of the qualified_module node itself is the full module name
                return { content: [{ type: "text", text: JSON.stringify(captures[0].node.text.replace(/\s+/g, ''), null, 2) }] };
            }
            return { content: [{ type: "text", text: JSON.stringify(null, null, 2) }] };
        },
        "getFunctionNames": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, false);
            const tree = purescriptTsParser.parse(code);
            const query = new Query(PureScriptLanguage, `(function name: (variable) @func.name)`);
            const captures = query.captures(tree.rootNode);
            const functionNames = captures.map(capture => capture.node.text);
            return { content: [{ type: "text", text: JSON.stringify(functionNames, null, 2) }] };
        },
        // Stubs for other Phase 1 handlers - to be implemented
        "getImports": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, true);
            const tree = purescriptTsParser.parse(code);
            const query = new Query(PureScriptLanguage, `(import module: (qualified_module) @import.path)`);
            const captures = query.captures(tree.rootNode);
            
            const imports = [];
            for (const capture of captures) {
                if (capture.name === 'import.path') {
                    const moduleNodes = capture.node.children.filter(child => child.type === 'module');
                    if (moduleNodes.length > 0) {
                        const fullPath = moduleNodes.map(n => n.text).join('.');
                        const moduleName = moduleNodes[0].text;
                        const submoduleName = moduleNodes.length > 1 ? moduleNodes[1].text : undefined;
                        imports.push({
                            module: moduleName,
                            submodule: submoduleName,
                            fullPath: fullPath
                        });
                    }
                }
            }
            return { content: [{ type: "text", text: JSON.stringify(imports, null, 2) }] };
        },
        "getTopLevelDeclarationNames": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, true); // true for module-oriented
            const tree = purescriptTsParser.parse(code);
            const querySource = `
    [
      (function name: (variable) @name)
      (data name: (type) @name)
      (class_declaration (class_head (class_name (type) @name)))
      (type_alias name: (type) @name)
      (foreign_import name: (variable) @name)
      (signature name: (variable) @name)
      (class_instance (instance_name) @name)
      (kind_value_declaration name: (type) @name)
    ]
    `;
            const query = new Query(PureScriptLanguage, querySource);
            const captures = query.captures(tree.rootNode);
            const declarationNames = captures.map(capture => capture.node.text).filter(Boolean);
            // Deduplicate names
            const uniqueNames = [...new Set(declarationNames)];
            return { content: [{ type: "text", text: JSON.stringify(uniqueNames, null, 2) }] };
        },
        "getIntegerLiterals": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, false);
            const tree = purescriptTsParser.parse(code);
            const integerLiterals = [];
    
            const query = new Query(PureScriptLanguage, `(integer) @integer.literal`);
            const captures = query.captures(tree.rootNode);
    
            captures.forEach(capture => {
                if (capture.name === 'integer.literal') {
                    integerLiterals.push(parseInt(capture.node.text, 10));
                }
            });
            return { content: [{ type: "text", text: JSON.stringify(integerLiterals, null, 2) }] };
        },
        "getWhereBindings": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, false); // Snippet-oriented
            const tree = purescriptTsParser.parse(code);
            const whereClausesText = [];
        
            // Query for 'where' keyword followed by a 'declarations' block within a function or let binding
            const querySource = `
              (function
                (where) @where_keyword
                (declarations) @declarations_block)
            `;
            // Also consider 'let' bindings with 'where' clauses, though less common for top-level 'where'
            // (let_binding (where) @where_keyword (declarations) @declarations_block)
    
            const query = new Query(PureScriptLanguage, querySource);
            const matches = query.matches(tree.rootNode);
        
            for (const match of matches) {
                const whereKeywordNode = match.captures.find(c => c.name === 'where_keyword')?.node;
                const declarationsNode = match.captures.find(c => c.name === 'declarations_block')?.node;
                
                if (whereKeywordNode && declarationsNode) {
                    // Construct the text from "where" keyword to the end of the declarations block
                    // This requires careful handling of start and end positions if they are not contiguous in the source text string
                    // For simplicity, if they are siblings and in order, we can take text from start of 'where' to end of 'declarations'
                    // A safer way is to combine their individual texts if they represent the full conceptual block
                    const fullWhereClauseText = `${whereKeywordNode.text} ${declarationsNode.text}`;
                    whereClausesText.push(fullWhereClauseText.trim());
                }
            }
            // Deduplicate, as some complex structures might yield multiple partial matches
            const uniqueWhereClauses = [...new Set(whereClausesText)];
            return { content: [{ type: "text", text: JSON.stringify(uniqueWhereClauses, null, 2) }] };
        },
        "getTopLevelDeclarations": async (args) => {
            if (!treeSitterInitialized) throw new Error("Tree-sitter not initialized.");
            const code = await getCodeFromInput(args, true); // true for module-oriented
            const tree = purescriptTsParser.parse(code);
    
            const querySource = `
                [
                  (function name: (variable) @name.function) @DeclValue
                  (data name: (type) @name.data_type) @DeclData
                  (class_declaration (class_head (class_name (type) @name.class))) @DeclClass
                  (type_alias name: (type) @name.type_alias) @DeclType
                  (newtype name: (type) @name.newtype) @DeclNewtype
                  (foreign_import name: (variable) @name.foreign) @DeclForeign
                  (signature name: (variable) @name.signature) @DeclSignature
                  (class_instance (instance_head (class_name) @name.instance_class (type_name)? @name.instance_type)) @DeclInstanceChain
                  (kind_value_declaration name: (type) @name.kind_sig) @DeclKindSignature
                  (derive_declaration) @DeclDerive
                  (type_role_declaration (type) @name.role_type (type_role)+ @name.role_value) @DeclRole
                  (operator_declaration (operator) @name.operator) @DeclFixity
                ]
            `;
            const query = new Query(PureScriptLanguage, querySource);
            const matches = query.matches(tree.rootNode);
            const rawDeclarations = [];
    
            for (const match of matches) {
                const mainCapture = match.captures.find(c => c.name.startsWith("Decl"));
                if (!mainCapture) continue;
    
                const declNode = mainCapture.node;
                const mappedType = mainCapture.name;
                const value = declNode.text; // This is the full text of the declaration node
    
                // Create a map of captures for efficient lookup
                // Store the full capture object {name, node} as node properties (like .text) are needed
                const allCapturesMap = new Map(match.captures.map(c => [c.name, c]));
    
                let finalName;
    
                // Prioritized list of capture names that directly provide the 'name'
                const singleNameCaptureKeys = [
                    "name.function", "name.data_type", "name.class", "name.type_alias",
                    "name.newtype", "name.foreign", "name.signature", "name.kind_sig",
                    "name.role_type", "name.operator"
                ];
    
                let foundSingleName = false;
                for (const key of singleNameCaptureKeys) {
                    if (allCapturesMap.has(key)) {
                        finalName = allCapturesMap.get(key).node.text;
                        foundSingleName = true;
                        break;
                    }
                }
    
                if (!foundSingleName) {
                    if (allCapturesMap.has("name.instance_class")) {
                        finalName = allCapturesMap.get("name.instance_class").node.text;
                        if (allCapturesMap.has("name.instance_type")) {
                            finalName += ` ${allCapturesMap.get("name.instance_type").node.text}`;
                        }
                    } else if (mappedType === "DeclDerive" || mappedType === "DeclFixity") {
                        // declNode is mainCapture.node, which is already available
                        const firstIdentNode = declNode.descendantsOfType("identifier")[0] ||
                                           declNode.descendantsOfType("type")[0] ||
                                           declNode.descendantsOfType("operator")[0];
                        finalName = firstIdentNode ? firstIdentNode.text : `complex_${mappedType.toLowerCase().replace('decl', '')}`;
                    } else {
                        finalName = "unknown"; // Default if no other specific name found
                    }
                }
                
                rawDeclarations.push({ name: finalName, type: mappedType, value, treeSitterType: declNode.type }); // Removed node property
            }
    
            let declarations = rawDeclarations; // Use rawDeclarations directly without consolidation
    
            // Apply filters if provided
            if (args.filters) {
                const { name, type, value } = args.filters;
                if (name) {
                    const nameRegex = new RegExp(name);
                    declarations = declarations.filter(d => nameRegex.test(d.name));
                }
                if (type) {
                    const typeRegex = new RegExp(type);
                    declarations = declarations.filter(d => typeRegex.test(d.type));
                }
                if (value) {
                    const valueRegex = new RegExp(value);
                    declarations = declarations.filter(d => valueRegex.test(d.value));
                }
            }
    
            return { content: [{ type: "text", text: JSON.stringify(declarations, null, 2) }] };
        },
        // --- New purs ide command wrapper handlers ---
        "pursIdeLoad": async (args) => {
            const params = args || {}; // If args is null/undefined, pass empty object for default load all
            const result = await sendCommandToPursIde({ command: "load", params });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeType": async (args) => {
            if (!args || typeof args.search !== 'string') {
                throw new Error("Invalid input: 'search' (string) is required for pursIdeType.");
            }
            const params = {
                search: args.search,
                filters: args.filters || [], // Default to empty filters array
                currentModule: args.currentModule
            };
            const result = await sendCommandToPursIde({ command: "type", params });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeCwd": async () => {
            const result = await sendCommandToPursIde({ command: "cwd" });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeReset": async () => {
            const result = await sendCommandToPursIde({ command: "reset" });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeQuit": async () => {
            let quitMessage = "purs ide server quit command initiated.";
            let pursIdeResponded = false;
    
            if (pursIdeProcess && pursIdeIsReady) {
                logToStderr("[pursIdeQuit] Attempting to send 'quit' command to purs ide server.", "debug");
                sendCommandToPursIde({ command: "quit" })
                    .then(res => {
                        pursIdeResponded = true;
                        logToStderr(`[pursIdeQuit] purs ide server responded to quit command: ${JSON.stringify(res)}`, 'debug');
                    })
                    .catch(err => {
                        logToStderr(`[pursIdeQuit] Error/No response from purs ide server for quit command: ${err.message}`, 'warn');
                    });
                
                // Wait a short period to allow purs ide server to shut down gracefully
                // or for the sendCommandToPursIde to potentially resolve/reject.
                await delay(250); // Increased slightly to 250ms
            } else {
                quitMessage = "No purs ide server was running or ready to send quit command to.";
                logToStderr("[pursIdeQuit] " + quitMessage, "info");
            }
    
            // Ensure our managed process is stopped regardless of purs ide's response
            if (pursIdeProcess) {
                logToStderr("[pursIdeQuit] Ensuring managed purs ide process is stopped.", "debug");
                pursIdeProcess.kill();
                pursIdeProcess = null;
                pursIdeIsReady = false;
                logPursIdeOutput("Managed purs ide server process stopped via pursIdeQuit tool.", "info");
                quitMessage += " Managed purs ide process has been stopped.";
            } else {
                if (!quitMessage.includes("No purs ide server was running")) {
                     quitMessage += " No managed purs ide process was found running to stop.";
                }
            }
            
            if (pursIdeResponded) {
                quitMessage += " purs ide server acknowledged quit.";
            } else {
                quitMessage += " purs ide server may not have acknowledged quit before process termination.";
            }
    
            return { content: [{ type: "text", text: JSON.stringify({ status_message: quitMessage, resultType: "success" }, null, 2) }] };
        },
        "pursIdeRebuild": async (args) => {
            if (!args || typeof args.file !== 'string') {
                throw new Error("Invalid input: 'file' (string) is required for pursIdeRebuild.");
            }
            const params = {
                file: args.file,
                actualFile: args.actualFile,
                codegen: args.codegen // purs ide server defaults to js if undefined
            };
            const result = await sendCommandToPursIde({ command: "rebuild", params });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeUsages": async (args) => {
            if (!args || typeof args.module !== 'string' || typeof args.namespace !== 'string' || typeof args.identifier !== 'string') {
                throw new Error("Invalid input: 'module', 'namespace', and 'identifier' (strings) are required for pursIdeUsages.");
            }
            const params = {
                module: args.module,
                namespace: args.namespace,
                identifier: args.identifier
            };
            const result = await sendCommandToPursIde({ command: "usages", params });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        },
        "pursIdeList": async (args) => {
            if (!args || typeof args.listType !== 'string') {
                throw new Error("Invalid input: 'listType' (string) is required for pursIdeList.");
            }
            const params = { type: args.listType };
            if (args.listType === "import") {
                if (typeof args.file !== 'string') {
                    throw new Error("'file' (string) is required when listType is 'import'.");
                }
                params.file = args.file;
            }
            const result = await sendCommandToPursIde({ command: "list", params });
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        }
    };
  • index.js:1158-1164 (registration)
    Handler for MCP 'tools/list' request returns the TOOL_DEFINITIONS array (filtered), which includes the schema/definition for 'pursIdeLoad'. This is how the tool is 'registered' and discoverable by MCP clients.
    if (method === 'tools/list') {
        const toolsToExclude = ['query_purescript_ast', 'query_purs_ide']; // Keep query_purs_ide for now, for direct access if needed
        const filteredToolDefinitions = TOOL_DEFINITIONS.filter(
            tool => !toolsToExclude.includes(tool.name)
        );
        return createSuccessResponse(id, { tools: filteredToolDefinitions });
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses key behavioral traits: it's a prerequisite action that enables other tools, implying it's a setup/initialization step. However, it doesn't mention potential side effects (e.g., loading time, memory usage), error handling, or what happens if modules fail to load. For a tool with no annotations, this is adequate but lacks depth.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, followed by prerequisite and usage guidance in clear, imperative sentences. Every sentence earns its place: the first explains what it does, the second states prerequisites, and the third gives timing instructions. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (initialization step for an IDE server), no annotations, and no output schema, the description is fairly complete. It covers purpose, prerequisites, and usage sequence, which are critical for an agent. However, it lacks details on expected outcomes (e.g., success indicators) or error cases, leaving some gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, so the baseline is 3. The description adds value by explaining the tool's purpose and prerequisites, which helps contextualize the 'modules' parameter (e.g., why loading modules is needed). However, it doesn't provide additional semantic details about the parameter beyond what the schema says (e.g., module naming conventions).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Load PureScript modules into the IDE server') and the purpose ('for type checking and completions'), distinguishing it from siblings like 'pursIdeList' (which likely lists modules) or 'pursIdeType' (which likely checks types). It uses precise verbs and resources.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It provides explicit usage instructions: 'PREREQUISITE: IDE server must be running' and 'ALWAYS run this first after starting the IDE server before using other pursIde* tools.' This clearly indicates when to use it (after server start, before other IDE tools) and distinguishes it from alternatives like 'start_purs_ide_server' (which starts the server).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/avi892nash/purescript-mcp-tools'

If you have feedback or need assistance with the MCP directory API, please join our Discord server