Skip to main content
Glama

export_md

Save the last property search results directly to a Markdown file in an approved directory, enabling report generation and offline analysis of Singapore HDB transactions and amenities.

Instructions

Export the last search results to a Markdown file. The file must be saved within a directory approved by the client.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filePathYesFull path for the Markdown file (e.g. '/home/user/exports/results.md')

Implementation Reference

  • The export_md tool handler — registered via server.tool('export_md', ...). It retrieves the last search results, formats them as a Markdown table (using formatters), builds a full Markdown document with query params and attribution, validates the output path against client-approved roots, and writes the file.
    server.tool(
      "export_md",
      "Export the last search results to a Markdown file. The file must be saved within a directory approved by the client.",
      {
        filePath: z
          .string()
          .describe("Full path for the Markdown file (e.g. '/home/user/exports/results.md')"),
      },
      async ({ filePath }, extra: ToolExtra) => {
        await logInfo(extra, `export_md: target="${filePath}"`);
    
        const lastSearch = state.getLastSearch();
        if (!lastSearch) {
          return { content: [{ type: "text" as const, text: "No search results to export. Run a search first." }] };
        }
        if (lastSearch.results.length === 0) {
          return { content: [{ type: "text" as const, text: "The last search returned no results \u2014 nothing to export." }] };
        }
    
        const check = await isPathAllowed(server.server, filePath);
        if (!check.allowed) {
          await logInfo(extra, `export_md: path denied \u2014 ${check.reason}`);
          const rootsList = check.roots.length > 0
            ? `\n\nAllowed directories:\n${check.roots.map((r) => `- ${r}`).join("\n")}`
            : "";
          return { content: [{ type: "text" as const, text: `Cannot write to "${filePath}". ${check.reason}${rootsList}` }] };
        }
    
        await logInfo(extra, `export_md: path allowed (root: ${check.root})`);
    
        let table: string;
        switch (lastSearch.type) {
          case "land-use":
            table = formatLandParcelsTable(lastSearch.results as LandParcel[]); break;
          case "nearby-amenities":
            table = formatNearbyAmenityTable(lastSearch.results as NearbyAmenity[]); break;
          default:
            table = formatHdbTable(lastSearch.results as HdbResaleRecord[]); break;
        }
    
        const queryLines = Object.entries(lastSearch.query)
          .filter(([, v]) => v != null)
          .map(([k, v]) => `- **${k}:** ${v}`);
        const heading = HEADING_MAP[lastSearch.type] ?? "Results";
        const attribution = ATTRIBUTION_MAP[lastSearch.type] ?? "(c) Urban Redevelopment Authority";
    
        const exportedAt = new Date().toLocaleString(undefined, {
          year: "numeric", month: "2-digit", day: "2-digit",
          hour: "2-digit", minute: "2-digit", second: "2-digit",
          timeZoneName: "short",
        });
    
        const md = [
          `# ${heading}`,
          "",
          `**Exported:** ${exportedAt}`,
          "",
          "## Query Parameters",
          "",
          ...queryLines,
          "",
          "## Results",
          "",
          table,
          "",
          "---",
          attribution,
          "",
        ].join("\n");
    
        try {
          await mkdir(path.dirname(path.resolve(filePath)), { recursive: true });
          await writeFile(path.resolve(filePath), md, "utf-8");
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          await logInfo(extra, `export_md: write failed \u2014 ${message}`);
          return { content: [{ type: "text" as const, text: `Failed to write file: ${message}` }] };
        }
    
        const resolved = path.resolve(filePath);
        const rowCount = lastSearch.results.length;
        const dataType = DATA_TYPE_LABELS[lastSearch.type] ?? lastSearch.type;
        await logInfo(extra, `export_md: wrote ${rowCount} ${dataType} records to ${resolved}`);
    
        return { content: [{ type: "text" as const, text: `Exported ${rowCount} ${dataType} records to:\n${resolved}` }] };
      },
    );
  • Input schema for export_md: a single required string parameter 'filePath' described as the full path for the Markdown file.
    {
      filePath: z
        .string()
        .describe("Full path for the Markdown file (e.g. '/home/user/exports/results.md')"),
    },
  • Registration function registerExportTools which calls server.tool('export_md', ...) and server.tool('export_csv', ...). Exported and called from src/index.ts line 20.
    export function registerExportTools(server: McpServer, state: SessionState): void {
      server.tool(
        "export_csv",
        "Export the last search results to a CSV file. The file must be saved within a directory approved by the client.",
        {
          filePath: z
            .string()
            .describe("Full path for the CSV file (e.g. '/home/user/exports/results.csv')"),
        },
        async ({ filePath }, extra: ToolExtra) => {
          await logInfo(extra, `export_csv: target="${filePath}"`);
    
          const lastSearch = state.getLastSearch();
          if (!lastSearch) {
            return { content: [{ type: "text" as const, text: "No search results to export. Run a search first." }] };
          }
          if (lastSearch.results.length === 0) {
            return { content: [{ type: "text" as const, text: "The last search returned no results \u2014 nothing to export." }] };
          }
    
          const check = await isPathAllowed(server.server, filePath);
          if (!check.allowed) {
            await logInfo(extra, `export_csv: path denied \u2014 ${check.reason}`);
            const rootsList = check.roots.length > 0
              ? `\n\nAllowed directories:\n${check.roots.map((r) => `- ${r}`).join("\n")}`
              : "";
            return { content: [{ type: "text" as const, text: `Cannot write to "${filePath}". ${check.reason}${rootsList}` }] };
          }
    
          await logInfo(extra, `export_csv: path allowed (root: ${check.root})`);
    
          let csv: string;
          switch (lastSearch.type) {
            case "land-use":
              csv = formatLandParcelsCsv(lastSearch.results as LandParcel[]); break;
            case "nearby-amenities":
              csv = formatNearbyAmenityCsv(lastSearch.results as NearbyAmenity[]); break;
            default:
              csv = formatHdbCsv(lastSearch.results as HdbResaleRecord[]); break;
          }
    
          try {
            await mkdir(path.dirname(path.resolve(filePath)), { recursive: true });
            await writeFile(path.resolve(filePath), csv, "utf-8");
          } catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            await logInfo(extra, `export_csv: write failed \u2014 ${message}`);
            return { content: [{ type: "text" as const, text: `Failed to write file: ${message}` }] };
          }
    
          const resolved = path.resolve(filePath);
          const rowCount = lastSearch.results.length;
          const dataType = DATA_TYPE_LABELS[lastSearch.type] ?? lastSearch.type;
          await logInfo(extra, `export_csv: wrote ${rowCount} ${dataType} records to ${resolved}`);
    
          return { content: [{ type: "text" as const, text: `Exported ${rowCount} ${dataType} records to:\n${resolved}` }] };
        },
      );
    
      server.tool(
        "export_md",
        "Export the last search results to a Markdown file. The file must be saved within a directory approved by the client.",
        {
          filePath: z
            .string()
            .describe("Full path for the Markdown file (e.g. '/home/user/exports/results.md')"),
        },
        async ({ filePath }, extra: ToolExtra) => {
          await logInfo(extra, `export_md: target="${filePath}"`);
    
          const lastSearch = state.getLastSearch();
          if (!lastSearch) {
            return { content: [{ type: "text" as const, text: "No search results to export. Run a search first." }] };
          }
          if (lastSearch.results.length === 0) {
            return { content: [{ type: "text" as const, text: "The last search returned no results \u2014 nothing to export." }] };
          }
    
          const check = await isPathAllowed(server.server, filePath);
          if (!check.allowed) {
            await logInfo(extra, `export_md: path denied \u2014 ${check.reason}`);
            const rootsList = check.roots.length > 0
              ? `\n\nAllowed directories:\n${check.roots.map((r) => `- ${r}`).join("\n")}`
              : "";
            return { content: [{ type: "text" as const, text: `Cannot write to "${filePath}". ${check.reason}${rootsList}` }] };
          }
    
          await logInfo(extra, `export_md: path allowed (root: ${check.root})`);
    
          let table: string;
          switch (lastSearch.type) {
            case "land-use":
              table = formatLandParcelsTable(lastSearch.results as LandParcel[]); break;
            case "nearby-amenities":
              table = formatNearbyAmenityTable(lastSearch.results as NearbyAmenity[]); break;
            default:
              table = formatHdbTable(lastSearch.results as HdbResaleRecord[]); break;
          }
    
          const queryLines = Object.entries(lastSearch.query)
            .filter(([, v]) => v != null)
            .map(([k, v]) => `- **${k}:** ${v}`);
          const heading = HEADING_MAP[lastSearch.type] ?? "Results";
          const attribution = ATTRIBUTION_MAP[lastSearch.type] ?? "(c) Urban Redevelopment Authority";
    
          const exportedAt = new Date().toLocaleString(undefined, {
            year: "numeric", month: "2-digit", day: "2-digit",
            hour: "2-digit", minute: "2-digit", second: "2-digit",
            timeZoneName: "short",
          });
    
          const md = [
            `# ${heading}`,
            "",
            `**Exported:** ${exportedAt}`,
            "",
            "## Query Parameters",
            "",
            ...queryLines,
            "",
            "## Results",
            "",
            table,
            "",
            "---",
            attribution,
            "",
          ].join("\n");
    
          try {
            await mkdir(path.dirname(path.resolve(filePath)), { recursive: true });
            await writeFile(path.resolve(filePath), md, "utf-8");
          } catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            await logInfo(extra, `export_md: write failed \u2014 ${message}`);
            return { content: [{ type: "text" as const, text: `Failed to write file: ${message}` }] };
          }
    
          const resolved = path.resolve(filePath);
          const rowCount = lastSearch.results.length;
          const dataType = DATA_TYPE_LABELS[lastSearch.type] ?? lastSearch.type;
          await logInfo(extra, `export_md: wrote ${rowCount} ${dataType} records to ${resolved}`);
    
          return { content: [{ type: "text" as const, text: `Exported ${rowCount} ${dataType} records to:\n${resolved}` }] };
        },
      );
    }
  • formatNearbyAmenityTable helper used by export_md for amenities data.
    export function formatNearbyAmenityTable(records: NearbyAmenity[]): string {
      if (records.length === 0) {
        return "No nearby amenities found matching your criteria.";
      }
    
      const header = "| Category | Name | Distance | Address |";
      const divider = "|---|---|---|---|";
      const rows = records.map((r) =>
        `| ${CATEGORY_LABELS[r.category] ?? r.category} | ${r.name} | ${formatDistance(r.distanceMeters)} | ${r.address ?? "—"} |`,
      );
    
      return [header, divider, ...rows].join("\n");
    }
  • formatLandParcelsTable helper used by export_md for land-use data.
    export function formatLandParcelsTable(parcels: LandParcel[]): string {
      if (parcels.length === 0) {
        return "No land parcels found in this area.";
      }
    
      const header = "| Land Use | Plot Ratio | Region | Planning Area | Subzone |";
      const divider = "|---|---|---|---|---|";
      const rows = parcels.map(
        (p) =>
          `| ${p.landUse} | ${p.grossPlotRatio ?? "N/A"} | ${p.region} | ${p.planningArea} | ${p.subzone} |`
      );
    
      return [header, divider, ...rows].join("\n");
    }
Behavior2/5

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

Without annotations, description carries full burden. It mentions a directory constraint but does not disclose overwrite behavior, error handling, or return value. 'Approved by client' is vague.

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?

Two concise sentences front-loaded with purpose. No redundancy or wasted words.

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

Completeness3/5

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

For a simple tool with one parameter and no output schema, the description covers purpose and a constraint. However, it lacks details on edge cases (e.g., empty results, path validation) and does not mention return or errors.

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

Parameters3/5

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

Schema coverage is 100% and the description adds no additional meaning beyond the schema's definition of filePath. Baseline score applies.

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?

Description clearly states verb 'Export' and resource 'last search results to a Markdown file'. It distinguishes from siblings like export_csv and analyze_results by specifying output format and source.

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

Usage Guidelines2/5

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

No explicit guidance on when to use this tool versus alternatives. The constraint about directory approval is mentioned but no comparison with export_csv for different output needs.

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

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/coolMukul/sg-property-mcp'

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