Hi Joel,
Yes, this works — I tested the pattern against our own stack before replying. The key point is that Palantir MCP is just a stdio JSON-RPC server, so you don’t need an LLM in the loop at all: you can drive it from a plain Python script with the official mcp client SDK (requires Python ≥ 3.10) and call its tools like ordinary functions.
The tools that map to your four steps (verified against com.palantir.mcp-server 0.386.0, which exposes 79 tools):
- Extract dataset columns →
get_foundry_dataset_schema ({"datasetRid": ...})
- Look up DD03T →
run_sql_query_on_foundry_dataset — note the argument is sqlQuery, and it defaults to 15 rows (max 1000). Pass rowLimit: 1000 or your EKKO field-name lookup will be silently truncated.
- Create the object type →
create_or_update_foundry_object_type
- Bulk rename → same tool; it’s an upsert keyed on
objectTypeId, so re-submit with updated display names and modificationType set to update.
One thing that isn’t obvious until you look the schema: globalBranchRid is a required field on the create/update tool, so the real loop is create_global_branch → create your object types on that branch → create_global_proposal for review. For a script generating dozens of object types that’s a feature rather than a burden — you get a reviewable diff instead of changing the ontology directly.
Minimal working skeleton (this exact pattern is what I ran in Claude Code):
import asyncio, json, os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client, get_default_environment
env = get_default_environment()
env["FOUNDRY_TOKEN"] = os.environ["FOUNDRY_TOKEN"]
server = StdioServerParameters(
command="npx",
args=["-y", "palantir-mcp", "--foundry-api-url",
"https://<your-stack>.palantirfoundry.com"],
env=env,
)
async def main():
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Introspect exact input schemas rather than hand-writing them
tools = {t.name: t for t in (await session.list_tools()).tools}
for dataset_rid, table_name in MY_SAP_DATASETS:
schema = await session.call_tool(
"get_foundry_dataset_schema", {"datasetRid": dataset_rid})
names = await session.call_tool(
"run_sql_query_on_foundry_dataset",
{"sqlQuery": f"SELECT FIELDNAME, DDTEXT FROM `{DD03T_RID}` "
f"WHERE TABNAME = '{table_name}' AND DDLANGUAGE = 'E'",
"rowLimit": 1000})
# ...build the object type payload from schema + names,
# targeting your globalBranchRid...
await session.call_tool(
"create_or_update_foundry_object_type", {...})
asyncio.run(main())
A few practical notes from the test:
- Rename
displayName, never apiName. Keep the raw SAP field name (EBELN, MATNR…) as the property API name and put the DD03T DDTEXT in the display name. Changing API names on an existing object type creates new properties rather than renaming, and breaks any OSDK code referencing them.
- Results aren’t pure JSON. Tool results are text content blocks with a human-readable preamble (e.g.
"Found 4 namespace(s):\n[…"), so strip the header line before json.loads(result.content[0].text).
- Pin the version (
npx -y palantir-mcp@x.y.z) and build payloads from list_tools() schemas rather than hard-coding them — the tool signatures aren’t a stable public API.
- Auth for scripts: set
FOUNDRY_TOKEN in the env (a user token or third-party app token with ontology-manager permissions); the interactive OAuth flow doesn’t apply headless.
For your DD04T/DD05S follow-up, create_or_update_foundry_link_type works the same way, so deriving link types from SAP foreign-key metadata (EKKO→EKPO on EBELN, KNA1 on KUNNR) fits the same loop.
Cheers,
Nicolas at Sibyl