Using OpenAI Agents SDK with DataDoe MCP
This tutorial shows how to build a small TypeScript agent with the OpenAI Agents JS SDK (opens in a new tab) and connect it to DataDoe MCP. The agent asks DataDoe for an export, downloads the file, queries it locally with DuckDB, and writes a JSON report.
Every file is listed in full, so you can build the project by copying the blocks below in order.
What you will build
By the end, you will have a small agent that:
- connects to DataDoe MCP with
hostedMcpTool - downloads an export file before the URL expires
- loads the data into DuckDB and runs SQL over it
- writes one final JSON file to
response/
Prerequisites
- Completion of the DataDoe MCP Overview setup, with an MCP key
- An OpenAI API key (opens in a new tab) with access to
gpt-5.4-mini - Node.js 24 or later, which runs TypeScript directly with no build step
Step 1: Set up the project
1mkdir datadoe-openai-agent && cd datadoe-openai-agent
2npm init -y
3npm pkg set type=module
4npm install @openai/agents zod @duckdb/node-api@1.5.5-r.4
5mkdir srcSetting type=module matters. Without it, Node refuses the files below with SyntaxError: Cannot use import statement outside a module. Pin the DuckDB release tag as shown. There is no plain 1.5.5 on npm.
Step 2: Configure your keys
Create a .env file in the project root:
1DATADOE_MCP_KEY=YOUR_DATADOE_MCP_KEY
2OPENAI_API_KEY=YOUR_OPENAI_API_KEYDATADOE_MCP_KEY: your MCP key from DataDoe MCP Integrations (opens in a new tab)OPENAI_API_KEY: your OpenAI API key
Node loads this file with --env-file when you run the app, so no extra dependency is needed. Keep .env out of version control.
Step 3: Create the local tools
DataDoe MCP supplies the data. Three local tools turn it into a workflow: one downloads the export, one queries it, and one writes the final artifact.
Create src/tools.ts:
1import { mkdir, writeFile } from 'node:fs/promises';
2import { resolve } from 'node:path';
3import { z } from 'zod';
4
5import { DuckDBInstance } from '@duckdb/node-api';
6import { tool } from '@openai/agents';
7
8export const downloadExport = tool({
9 name: 'download_export',
10 description:
11 'URGENT: DataDoe export links expire quickly, so call this immediately after you get one. Downloads the file and returns its local path.',
12 parameters: z.object({
13 url: z.string().describe('The export download URL returned by DataDoe MCP.'),
14 filename: z.string().describe('File name to save as, for example "sales.csv".')
15 }),
16 async execute({ url, filename }) {
17 const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
18 const target = resolve('data', safeName);
19
20 const response = await fetch(url);
21
22 if (!response.ok) {
23 throw new Error(`Download failed: HTTP ${response.status}`);
24 }
25
26 await mkdir('data', { recursive: true });
27 await writeFile(target, Buffer.from(await response.arrayBuffer()));
28
29 return `Saved to ${target}`;
30 }
31});
32
33export const queryCsv = tool({
34 name: 'query_csv',
35 description:
36 'Runs a read-only SQL query against a downloaded CSV file using DuckDB. The file is exposed as a table named "data".',
37 parameters: z.object({
38 filePath: z.string().describe('Local path returned by download_export.'),
39 sql: z
40 .string()
41 .describe(
42 'A SELECT query against the table "data", for example: SELECT * FROM data LIMIT 5'
43 )
44 }),
45 async execute({ filePath, sql }) {
46 const instance = await DuckDBInstance.create(':memory:');
47 const connection = await instance.connect();
48
49 try {
50 await connection.run('CREATE TABLE data AS SELECT * FROM read_csv(?)', [filePath]);
51 const reader = await connection.runAndReadAll(sql);
52
53 // Use the Json variant: DuckDB returns BIGINT as a JS BigInt,
54 // which JSON.stringify cannot serialize.
55 return JSON.stringify(reader.getRowObjectsJson());
56 } finally {
57 connection.closeSync();
58 }
59 }
60});
61
62export const saveReport = tool({
63 name: 'save_report',
64 description:
65 'Writes the final report to ./response as JSON. Call this once, at the end, with the complete result.',
66 parameters: z.object({
67 summary: z.string().describe('A short summary of what the data shows.'),
68 rows: z
69 .array(
70 z.object({
71 asin: z.string(),
72 unitsSold: z.number()
73 })
74 )
75 .describe('Ranked best first.')
76 }),
77 async execute({ summary, rows }) {
78 await mkdir('response', { recursive: true });
79 const filePath = `response/report-${Date.now()}.json`;
80 await writeFile(filePath, JSON.stringify({ summary, rows }, null, 2), 'utf8');
81
82 return `Report written to ${filePath}`;
83 }
84});Use getRowObjectsJson(), not getRowObjects(). Any SUM() or COUNT() comes back as a BIGINT, which becomes a JS BigInt that JSON.stringify refuses to serialize.
Step 4: Create the agent
Create src/index.ts. This is the complete entry point:
1import { Agent, hostedMcpTool, run } from '@openai/agents';
2
3import { downloadExport, queryCsv, saveReport } from './tools.ts';
4
5const mcpKey = process.env.DATADOE_MCP_KEY;
6
7if (!mcpKey) {
8 throw new Error('DATADOE_MCP_KEY is not set. Add it to your .env file.');
9}
10
11const agent = new Agent({
12 name: 'Amazon Data Analyst',
13 model: 'gpt-5.4-mini',
14 instructions: [
15 'You analyze Amazon seller data through the DataDoe MCP server.',
16 'To answer a data question: create an export, take its download URL,',
17 'save it with download_export, then analyze it with query_csv.',
18 'Finish by calling save_report exactly once with the complete result.',
19 'Never write the final JSON directly in your reply.'
20 ].join(' '),
21 tools: [
22 hostedMcpTool({
23 serverLabel: 'datadoe',
24 serverUrl: 'https://mcp.datadoe.com/mcp/v1',
25 headers: { 'datadoe-mcp-key': mcpKey },
26 requireApproval: 'never'
27 }),
28 downloadExport,
29 queryCsv,
30 saveReport
31 ]
32});
33
34const prompt =
35 process.argv.slice(2).join(' ') || 'Which of my ASINs sold the most units in the last 7 days?';
36
37const result = await run(agent, prompt);
38
39console.log(result.finalOutput);Note the local import path ends in .ts. Node requires the explicit extension when it runs TypeScript directly.
requireApproval: 'never' is the line that keeps an unattended script running. With approval enabled, every hosted MCP call waits for a human who is not there. The SDK treats an omitted value as 'never' today, so writing it out protects the script from a future change in that default.
Step 5: Run the app
1node --env-file=.env src/index.tsYou can also pass your own question:
1node --env-file=.env src/index.ts "Which ASINs sold the most units in the last 30 days?"The run follows this shape:
- The agent asks DataDoe MCP for the export it needs.
download_exportsaves the file before the link expires.query_csvloads it into DuckDB and runs SQL to rank the ASINs.save_reportwrites the final JSON to./response/.
The file holds the summary and the ranked rows together, so a downstream script or cron job can read it directly.
Optional: stream the output
For a live view of the agent's reasoning, replace the last two lines of src/index.ts with a streamed run:
1const stream = await run(agent, prompt, { stream: true });
2
3stream.toTextStream({ compatibleWithNodeStreams: true }).pipe(process.stdout);
4
5await stream.completed;Use either
toTextStream()or afor awaitloop over the stream. Never use both on the same run. Reading a stream twice throwsERR_INVALID_STATEat runtime, and TypeScript will not warn you.
A simple mental model
If you want to adapt this pattern for another DataDoe-powered agent, keep the same split:
- DataDoe MCP for source data
- local tools for downloading, transforming, and validating
- one dedicated output tool for the final artifact
Each piece stays replaceable. Swap DuckDB for another query engine, or the JSON writer for a database insert, without touching the agent's instructions.
Related resources
- DataDoe MCP Overview
- DataDoe MCP Integrations (opens in a new tab)
- OpenAI Agents JS SDK documentation (opens in a new tab)
- DuckDB Node client (opens in a new tab)
DataDoe MCP resources
Check the following resources for more information:
- MCP server URL:
https://mcp.datadoe.com/mcp/v1 - Interactive Data Scheme
- Data Scheme JSON: https://api.datadoe.com/api/v1/spec/data-scheme
- Need help? Use the contact form

