StackOne AI provides a unified interface for accessing various SaaS tools through AI-friendly APIs.
- Search and execute: find an action in natural language and run it, so a catalog of hundreds of tools never has to fit in a model's context
- Account discovery: an API key is enough — linked accounts are found for you
- MCP-backed: tools are fetched at runtime, and the schema a model is shown is the schema the server served
- Filtering by account, provider and glob action pattern
- Integrations: OpenAI functions, LangChain, LangGraph, Pydantic AI
- Python 3.11+
uv add stackone-aiFramework adapters are extras, imported lazily: uv add 'stackone-ai[langchain]'
(LangGraph uses this one too) or uv add 'stackone-ai[pydantic-ai]'.
Set STACKONE_API_KEY and go. The SDK finds your linked accounts itself — you
only pass an account id when you want to narrow things down.
from stackone_ai import StackOneToolSet
toolset = StackOneToolSet()
# 1. Find an action. Each hit carries the JSON Schema for its own arguments.
hits = toolset.search("list recent comments", top_k=3)
# [{"action_id": "linear_list_comments",
# "description": "Returns a page of Linear comments as a connection object ...",
# "similarity_score": 0.86,
# "example_request": {"action_id": "linear_list_comments"},
# "input_schema": {"type": "object", "properties": {"body": {...}}}}, ...]
# 2. Run it. Build the arguments from input_schema.
result = toolset.execute("linear_list_comments", {"body": {"variables": {"first": 25}}})
result["data"] # the provider's payload — a failed call raises insteadsearch() asks every linked connector and returns actions ranked by
similarity_score, so a catalog of hundreds of tools never has to fit in a
model's context. This is the recommended way to use the SDK.
Every block below runs as written against any linked account — the *_list_*
filter discovers whatever your key can reach rather than assuming a provider.
Each has a matching runnable script in examples/.
OpenAI
Needs no stackone-ai extra — just uv add openai.
from openai import OpenAI
from stackone_ai import StackOneToolSet
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["*_list_*"])
openai_tools = tools.to_openai()[:20] # keep the catalog inside the context window
client = OpenAI()
messages = [{"role": "user", "content": "Use a tool to list a few records, then summarise them."}]
response = client.chat.completions.create(
model="gpt-5.4", messages=messages, tools=openai_tools, tool_choice="auto"
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True)) # the assistant turn comes first
messages.extend(tools.execute_openai_tool_calls(message.tool_calls))to_openai() emits Chat Completions function tools, and
execute_openai_tool_calls() runs the calls the model makes and returns the tool
messages to send back. A failed call becomes an error message the model can read and
retry from, rather than an exception. Chat Completions accepts at
most 128 tools, so filter before binding. See
examples/openai_integration.py for the full
round trip, including feeding results back for a final answer.
LangChain
uv add 'stackone-ai[langchain]' langchain-openaifrom langchain_openai import ChatOpenAI
from stackone_ai import StackOneToolSet
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["*_list_*"])
model = ChatOpenAI(model="gpt-5.4").bind_tools(tools.to_langchain())
response = model.invoke("Use a tool to list a few records.")
for call in response.tool_calls:
tool = tools.get_tool(call["name"])
print(tool.execute(call["args"]))Pydantic AI
uv add 'stackone-ai[pydantic-ai]'from pydantic_ai import Agent
from stackone_ai import StackOneToolSet
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["*_list_*"]).to_pydantic_ai()
agent = Agent("openai:gpt-5.4", tools=tools)
print(agent.run_sync("Use a tool to list a few records, then summarise them.").output)toolset.pydantic_ai() is the same thing for the unfiltered catalog, parallel to
.openai() and .langchain().
LangGraph
LangGraph consumes LangChain tools, so it goes through the same adapter.
uv add 'stackone-ai[langchain]' langchain langchain-openaifrom langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from stackone_ai import StackOneToolSet
toolset = StackOneToolSet()
tools = toolset.fetch_tools(actions=["*_list_*"]).to_langchain()
agent = create_agent(ChatOpenAI(model="gpt-5.4"), tools)
result = agent.invoke({"messages": [("user", "Use a tool to list a few records.")]})
print(result["messages"][-1].content)fetch_tools() takes three filters. They combine with AND, and all of them are
applied locally to one cached listing — changing a filter never refetches.
toolset = StackOneToolSet()
toolset.fetch_tools() # every tool, every active account
toolset.fetch_tools(providers=["linear"]) # one connector
toolset.fetch_tools(actions=["linear_list_*"]) # one connector's list actions
toolset.fetch_tools(actions=["linear_get_issue"]) # exactly one tool
toolset.fetch_tools(providers=["linear"],
actions=["*_get_*"]) # both filters, AND-ed
toolset.fetch_tools(account_ids=["acc-123", "acc-456"])account_ids— restrict to these accounts. Omit it and the SDK discovers your active accounts. An empty list means "no filter", not "no accounts". A single failing account is logged and skipped, not fatal.providers— matched case-insensitively as a full prefix, soproviders=["linear"]and["LINEAR"]are the same, and a connector whose name contains an underscore must be spelled in full (["browser_linkedin"], not["browser"]). No globs here — useactionsfor that.actions— glob patterns, matched case-sensitively against the whole tool name. Exact (["linear_get_issue"]), prefix (["linear_*"]), infix (["*_list_*"]), and character classes (["linear_[lg]*"]) all work. Multiple patterns are OR'd together.
# Chaining: set_accounts() scopes every later call on this toolset.
toolset.set_accounts(["acc-123"])
tools = toolset.fetch_tools(providers=["linear"])The SDK exposes the same actions through two surfaces. They take different argument shapes, because each mirrors the schema the server served for it. Both return the payload itself.
search() + toolset.execute() |
fetch_tools() + tool.execute() |
|
|---|---|---|
| Schema to read | input_schema on each hit |
tool.parameters.properties |
| Argument shape | nested — {"body": {"variables": {...}}} |
flat, prefixed — body_variables, path_id |
| Best for | agents that discover actions at run time | binding a fixed, filtered set of tools to a model |
# Same action, both surfaces:
toolset.execute("linear_list_comments", {"body": {"variables": {"first": 25}}})
tool = toolset.fetch_tools(actions=["linear_list_comments"]).get_tool("linear_list_comments")
tool.execute({"body_variables": {"first": 25}})Mixing them fails silently in one direction. A fetch_tools() tool also
accepts the nested form. But flat keys like body_variables passed to
toolset.execute() are not an error — they are dropped, and you get the server's
defaults.
For more examples, check out the examples/ directory:
- OpenAI Integration — OpenAI function calling
- Search and Execute — the recommended flow
- LangChain Integration — LangChain tools
- LangGraph Integration — LangGraph agent
- Pydantic AI Integration — Pydantic AI agent
- Auth Management — API key and account ID patterns
Development uses uv for Python and make as the task runner.
# Core only
make install
# Everything: adapters, examples and dev tooling
make install extras=1make # list all targets
make format # fix lint, format, and type check
make test # run all testsLinting, type checking and tests run in CI on every push; there are no git hooks.
Tests that exercise the MCP mock server need its Node dependencies
(pnpm provides tsx, which runs the server):
pnpm installWithout these, those tests fail rather than skip.
Apache 2.0 License