Skip to main content
Firebase Genkit is Google’s open-source framework for building AI-powered JavaScript/TypeScript applications. Unlike end-user AI tools that add the Transform MCP server through a settings screen, Genkit connects to it from application code: you create an MCP client and hand its tools to a model call. Because Transform is a hosted remote MCP server, there is nothing to install or run locally. You point the client at the server URL and authenticate with your Unstructured API key. Genkit ships the building block that makes this work out of the box, so you do not need a custom transport: the @genkit-ai/mcp package’s createMcpClient function connects to a remote MCP server over the streamable HTTP transport, and its requestInit setting carries your Unstructured API key as an Authorization: Bearer header on every request, including the initial initialize handshake.

Requirements

Before you begin, you must have the following:
  • Node.js 20 or later on your local development machine. To check, in your terminal, run node -v or node --version. Install Node.js.
  • An Unstructured API key, which the Transform MCP server uses as a bearer token. Get an API key.
  • A Gemini API key for the model that drives the tools. Get an API key.

Install the packages

In your terminal, install Genkit, the MCP plugin, and the Google GenAI model plugin:
npm install genkit @genkit-ai/mcp @genkit-ai/google-genai
Genkit is in active development and its APIs change between releases. This guide was verified against @genkit-ai/mcp version 1.39.0. If you install a different version, check the Genkit release notes for changes.

Connect to the Transform MCP server

Store your API keys in environment variables rather than hard-coding them:
export UNSTRUCTURED_API_KEY="<your-unstructured-api-key>"
export GEMINI_API_KEY="<your-gemini-api-key>"
In your application code, create an MCP client for the Transform MCP server. The server connection settings go inside the mcpServer object, and the requestInit headers attach the Authorization: Bearer header to every request, so the handshake and the tool calls are both authenticated:
import { createMcpClient } from "@genkit-ai/mcp";

const transformClient = createMcpClient({
  name: "unstructured_transform",
  mcpServer: {
    url: "https://mcp.transform.unstructured.io",
    requestInit: {
      headers: {
        Authorization: `Bearer ${process.env.UNSTRUCTURED_API_KEY}`,
      },
    },
  },
});
Use the server URL exactly as shown. Do not add /mcp or /sse to the end of the URL.Also, the server connection settings must be nested inside the mcpServer object as shown. Passing url at the top level of the createMcpClient options throws TypeError: Cannot read properties of undefined (reading 'disabled') in version 1.39.0.

Understand the transform job lifecycle

Transforming a document is an asynchronous, multi-step flow. Four of the steps are MCP tool calls, and two are plain HTTP transfers that are not MCP calls:
  1. request_file_upload_url: Returns a pre-signed upload_url and a file_ref for a local file.
  2. An HTTP PUT of the raw file bytes to the pre-signed upload_url. This is not an MCP call.
  3. transform_files: Starts a transform job for one or more file_ref values (or public HTTP(S) URLs) and returns a job_id.
  4. check_transform_status: Reports whether the job is SCHEDULED, IN_PROGRESS, or COMPLETED.
  5. get_transform_results: Returns a pre-signed download_url for the Markdown output of each transformed file.
  6. An HTTP GET of the Markdown from the pre-signed download_url. This is not an MCP call.
An agent cannot perform the two raw HTTP transfers through MCP tools alone, and an unpaced polling loop wastes model calls. Define a small wait_seconds tool alongside the MCP tools so the model can pause between status checks.
The pre-signed upload_url and download_url carry their own credentials in the URL itself. The PUT and GET must not send the Authorization header, or the storage service rejects them.

Build the application

The following complete example gives Gemini the Transform tools plus a wait_seconds pacing tool, then asks it to parse a PDF from a public URL. Save it as index.mjs:
import { setTimeout as sleep } from "node:timers/promises";

import { googleAI } from "@genkit-ai/google-genai";
import { createMcpClient } from "@genkit-ai/mcp";
import { genkit, z } from "genkit";

const ai = genkit({ plugins: [googleAI()] });

const transformClient = createMcpClient({
  name: "unstructured_transform",
  mcpServer: {
    url: "https://mcp.transform.unstructured.io", // root URL - do not append /mcp
    requestInit: {
      headers: {
        Authorization: `Bearer ${process.env.UNSTRUCTURED_API_KEY}`,
      },
    },
  },
});

const waitSeconds = ai.defineTool(
  {
    name: "wait_seconds",
    description:
      "Pause before the next status check. Transform jobs take 30 seconds to a few " +
      "minutes; call this between check_transform_status calls instead of polling " +
      "in a tight loop. Use 30 seconds unless told otherwise.",
    inputSchema: z.object({ seconds: z.number().int().min(1).max(120) }),
    outputSchema: z.object({ waitedSeconds: z.number() }),
  },
  async ({ seconds }) => {
    await sleep(seconds * 1000);
    return { waitedSeconds: seconds };
  },
);

await transformClient.ready();
const transformTools = await transformClient.getActiveTools(ai);

const { text } = await ai.generate({
  model: googleAI.model("gemini-3.1-flash-lite"),
  system:
    "You parse documents with the Unstructured Transform MCP server. Pass public " +
    "https:// file URLs straight to transform_files. It returns a job_id; poll with " +
    "check_transform_status, calling wait_seconds(30) between checks - jobs take 30 " +
    "seconds to a few minutes. When the job completes, call get_transform_results " +
    "and report the parsed content back to the user.",
  prompt:
    "Parse this PDF with Unstructured Transform: https://arxiv.org/pdf/1706.03762 " +
    "When it completes, tell me the document title and the first three section " +
    "headings from the parsed output.",
  tools: [...transformTools, waitSeconds],
  maxTurns: 20,
});

console.log(text);
await transformClient.disable();
On the Gemini API free tier, some models allow as few as 5 requests per minute, and the model spends one request per step of the workflow. The wait_seconds(30) pacing in the system prompt keeps a polling loop from hitting that limit; if you see 429 RESOURCE_EXHAUSTED or transient 503 errors, increase the wait, retry later, or choose a different model from the Gemini model list.

Run the application

From the directory that contains index.mjs:
node index.mjs
The model starts the transform job, polls at a measured pace with wait_seconds, fetches the results, and prints its answer. A successful run ends with output like this:
The PDF (Attention Is All You Need) has been parsed successfully.

*   Document Title: Attention Is All You Need
*   First Three Section Headings:
    1.  1 Introduction
    2.  2 Background
    3.  3 Model Architecture

Next steps

  • Control Transform file parsing output: Control how the Unstructured Transform MCP server instructs Transform to partition, enrich, chunk, and embed the data based on your files.
  • Control Transform generated sample code: Control how the Unstructured Transform MCP server generates sample curl or Python code that demonstrates how to use Transform to partition, enrich, chunk, and embed the data based on your files.

Questions? Need help?