Give an AI agent market data over MCP

To give an AI agent market data, point its MCP client at the Narwhal endpoint, list the tools and let the model call them. Each tool has an input schema and an output schema, and each result comes back as structured JSON.

List the tools the agent can use

An agent needs to know what it may call. The MCP endpoint answers tools/list with one entry per tool, and each entry has a name, a title, a description, an inputSchema and an outputSchema. This request lists them. The full response is long, so the example below keeps one tool and cuts its schema to the country argument.

curl, list the MCP tools
curl -s -X POST https://api.narwhalapi.com/mcp \
  -H "Authorization: Bearer $NARWHAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Response, one tool, trimmed
{
 "jsonrpc": "2.0",
 "id": 1,
 "result": {
  "tools": [
   {
    "annotations": {
     "readOnlyHint": true,
     "destructiveHint": false
    },
    "description": "Latest headline consumer price index. `index_base` names the base, for example 1982-84=100. When `source` is present, the data came through a distributor and `released_on` is the distributor's publication date, not the original publisher's release date.",
    "inputSchema": {
     "properties": {
      "country": {
       "pattern": "^[A-Z]{3}$",
       "title": "Country",
       "type": "string"
      }
     },
     "required": [
      "country"
     ],
     "type": "object",
     "additionalProperties": false
    },
    "name": "economics_get_cpi",
    "title": "Get headline CPI"
   }
  ]
 }
}

The names carry the data area as a prefix, such as economics_, fx_, commodities_ and filings_. Among the tools in the list are economics_get_cpi, fx_convert_currency, commodities_get_spot_price and filings_list_latest_filings. The annotations mark this tool as read-only, so a call cannot change anything in the account's data.

Read the input and output schemas

The inputSchema is a JSON Schema for the arguments. In the example, country is required, must match the pattern ^[A-Z]{3}$ (a three-letter code such as USA), and additionalProperties is false, so an argument the tool does not define is rejected. The full schema also has optional period and version_id arguments. An agent framework can pass inputSchema to the model as the shape of the tool call.

The outputSchema describes the result. For this tool it names country, period, value, index_base and released_on as required fields, and it allows a source object that is present only when a distributor supplied the data. value is a string, so the figure keeps all its digits. An agent that reads structuredContent can rely on those fields without parsing prose, and a test can check each result against the schema before the agent uses the number in an answer.

Call one tool

tools/call takes the tool name and its arguments. This request asks for the latest United States CPI.

curl, call economics_get_cpi
curl -s -X POST https://api.narwhalapi.com/mcp \
  -H "Authorization: Bearer $NARWHAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"economics_get_cpi","arguments":{"country":"USA"}}}'
Response
{
 "jsonrpc": "2.0",
 "id": 2,
 "result": {
  "content": [
   {
    "text": "{\"country\":\"USA\",\"period\":\"2026-09\",\"value\":\"336.940\",\"index_base\":\"1982-84=100\",\"released_on\":\"2026-10-14\"}",
    "type": "text"
   }
  ],
  "isError": false,
  "structuredContent": {
   "country": "USA",
   "period": "2026-09",
   "value": "336.940",
   "index_base": "1982-84=100",
   "released_on": "2026-10-14"
  }
 }
}

structuredContent is the result as an object that matches the outputSchema. content holds the same object serialized as JSON text, for clients that read text only, and isError is false. The figure is the index value for the period 2026-09, published on released_on, with its base in index_base. The REST route GET /v1/economics/USA/cpi returns the same object.

What a failed call looks like

A call with an invalid argument still returns a JSON-RPC result. isError is true, and the problem object sits in structuredContent, with a status, a code and a detail that says what was wrong. The request used US instead of USA.

Response to economics_get_cpi with country US
{
 "jsonrpc": "2.0",
 "id": 3,
 "result": {
  "content": [
   {
    "text": "{\"type\":\"https://narwhalapi.com/problems/invalid-query\",\"title\":\"Invalid query\",\"status\":400,\"detail\":\"country: String should match pattern '^[A-Z]{3}$'\",\"code\":\"invalid_query\",\"request_id\":\"0b2d26aa-f433-41a5-b341-d473ad263ac9\"}",
    "type": "text"
   }
  ],
  "isError": true,
  "structuredContent": {
   "type": "https://narwhalapi.com/problems/invalid-query",
   "title": "Invalid query",
   "status": 400,
   "detail": "country: String should match pattern '^[A-Z]{3}$'",
   "code": "invalid_query",
   "request_id": "0b2d26aa-f433-41a5-b341-d473ad263ac9"
  }
 }
}

Because the detail names the pattern, a model can correct its own arguments and call again. Hand the error text back to the model as the tool result and let it retry once, rather than letting an exception end the whole run. A second failure with the same arguments is a signal to stop and report the detail to the user.

Pass the schemas to a model and run its calls

The script below connects to the endpoint, keeps three tools from the list in the shape most model APIs expect, and defines call_tool, which your agent loop runs for each tool call the model makes. It returns structuredContent, or a short error for the model.

Python, tool definitions from tools/list and a tool runner
import json, os, urllib.request

MCP = "https://api.narwhalapi.com/mcp"
HEADERS = {
    "Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}",
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream",
}

def rpc(method, params=None, id=1):
    body = {"jsonrpc": "2.0", "id": id, "method": method}
    if params:
        body["params"] = params
    req = urllib.request.Request(MCP, json.dumps(body).encode(), HEADERS)
    return json.load(urllib.request.urlopen(req))["result"]

WANTED = {"economics_get_cpi", "fx_convert_currency", "commodities_get_spot_price"}
tool_specs = [
    {"name": t["name"], "description": t["description"], "input_schema": t["inputSchema"]}
    for t in rpc("tools/list")["tools"] if t["name"] in WANTED
]
print([t["name"] for t in tool_specs])

def call_tool(name, arguments):
    result = rpc("tools/call", {"name": name, "arguments": arguments}, id=2)
    if result["isError"]:
        return {"error": result["structuredContent"]["detail"]}
    return result["structuredContent"]

print(call_tool("economics_get_cpi", {"country": "USA"}))
print(call_tool("economics_get_cpi", {"country": "US"}))
Output
['economics_get_cpi', 'fx_convert_currency', 'commodities_get_spot_price']
{'country': 'USA', 'period': '2026-09', 'value': '336.940', 'index_base': '1982-84=100', 'released_on': '2026-10-14'}
{'error': "country: String should match pattern '^[A-Z]{3}$'"}

The connection steps for each client are in the agent access guide. The Claude Agent SDK tutorial and the LangChain tutorial show a client that does this wiring for you.

Can an agent change data through these tools?

No. The tool shown has readOnlyHint set to true and destructiveHint set to false. Every tool in the list carries those same two hints. Narwhal's MCP tools only read data, and the same account, key and credit serve the REST data APIs.

Does listing tools use a request?

No. Listing the tools costs nothing. Each successful tools/call uses one request on the same key as REST, so an agent that calls three tools for a question uses three requests. Keep the tool set small, so the model sees only the tools the question needs.

Which tool should the agent use for a question?

The description of each tool says what it returns, and the name starts with the data area. Pass the descriptions to the model as they are. For a price, the model picks commodities_get_spot_price or fx_convert_currency. For a company filing, it picks filings_list_latest_filings. If you need to limit scope, filter the list by prefix before you give it to the model, as the script does with WANTED.

Published .

Next steps