Help us improve
Share bugs, ideas, or general feedback.
From launchdarkly
Guides creating tool definitions for AI agents using function calling, attaching them to LaunchDarkly config variations, and verifying the setup.
npx claudepluginhub launchdarkly/ai-toolingHow this skill is triggered — by the user, by Claude, or both
Slash command
/launchdarkly:toolsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You're using a skill that will guide you through adding capabilities to your agents through tools (function calling). Your job is to identify what your agent needs to do, create tool definitions, attach them to variations, and verify they work.
Guides designing AI agent tools with principles for workflows, context optimization, naming, input/output schemas, error handling, and evaluation.
Designs AI agent tools covering JSON Schema best practices, descriptions, validation, error handling, and MCP standard for Anthropic, OpenAI, LangChain.
Guides creation of LaunchDarkly AI configs with mode selection, variations, and verification. Includes bias-toward-action workflow and targeting warnings.
Share bugs, ideas, or general feedback.
You're using a skill that will guide you through adding capabilities to your agents through tools (function calling). Your job is to identify what your agent needs to do, create tool definitions, attach them to variations, and verify they work.
This skill requires the remotely hosted LaunchDarkly MCP server to be configured in your environment.
Required MCP tools:
create-ai-tool -- create a new tool definition with a schemaupdate-ai-config-variation -- attach tools to a config variationget-ai-config -- verify tools are attached to the variationOptional MCP tools:
list-ai-tools -- browse existing tools in the projectget-ai-tool -- inspect a specific tool's schemaWhat should the agent be able to do?
If the user asks to check existing tools first, or you have no codebase context about what tools exist, follow this exact order:
list-ai-tools -- explore what existscreate-ai-tool -- create the new tool (with a key different from existing ones)update-ai-config-variation -- attach itget-ai-config -- verifyCall list-ai-tools as your first tool call before any creation. Never stop after listing alone -- always proceed through all four steps.
Use create-ai-tool with:
key -- unique identifier for the tooldescription -- clear description (the LLM uses this to decide when to call the tool)schema -- raw JSON Schema (do NOT use the OpenAI function calling wrapper):{
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"limit": {"type": "integer", "default": 10}
},
"required": ["query"]
}
Use update-ai-config-variation to attach tools. Pass only the tools field. Do not bundle instructions, messages, model, or parameters into this PATCH unless the user has explicitly asked you to also update those fields. Those fields may have been edited in the LaunchDarkly UI since the variation was created, and including them in a tool-attachment PATCH will silently clobber the UI edits.
{
"projectKey": "my-project",
"configKey": "support-chatbot",
"variationKey": "default",
"tools": [
{"key": "search-knowledge-base", "version": 1}
]
}
If you observe a UI-clear bug where attaching tools wipes other fields, do not work around it by re-sending those fields from the previous get-ai-config response — that masks the bug and can resurrect stale values that the user has since edited. Report the bug instead.
get-ai-tool to confirm the tool exists with a valid schemaget-ai-config to confirm the tool is attached to the variation (check tools in the variation's output)Report results:
LaunchDarkly stores the tool schema once — the flat {type, name, description, parameters} shape you passed to create-ai-tool. Your application reads it back via config.model.parameters.tools (completion mode) or agent_config.model.parameters.tools (agent mode), then converts to the shape the provider SDK expects. LaunchDarkly never makes the provider call; your code does. The handlers that implement each tool also stay in application code — LaunchDarkly stores the schema, your application owns the behavior.
| Provider / framework | Target shape | Where it goes on the call |
|---|---|---|
| OpenAI Chat Completions (direct SDK) | {type: "function", function: {name, description, parameters}} | top-level tools=[...] |
| Anthropic direct SDK | {name, description, input_schema} — rename parameters → input_schema | top-level tools=[...] |
| Bedrock Converse | {toolSpec: {name, description, inputSchema: {json: parameters}}} | inside toolConfig.tools=[...] |
Gemini (google-genai) | {function_declarations: [{name, description, parameters}]} (Python) / {functionDeclarations: [...]} (Node) | GenerateContentConfig.tools=[...] |
| OpenAI Responses API | LaunchDarkly's flat shape passes through unchanged | top-level tools=[...] |
| LangChain / LangGraph | createLangChainModel(config) (Node) / create_langchain_model(config) (Python) and pass ai_config.tools (or your own StructuredTool list) into bind_tools(...) / create_react_agent(tools=[...]) | framework-native; no per-call conversion |
| Strands Agents | LaunchDarkly's flat shape; drop parameters.tools before passing params to the Strands model class (AnthropicModel, OpenAIModel) — Python @tool-decorated callables stay in code | Agent(tools=[...]) constructor; no per-call conversion |
Minimal conversion snippets (Python):
ld_tools = (ai_config.model.to_dict().get("parameters") or {}).get("tools", []) or []
# OpenAI Chat Completions
openai_tools = [
{
"type": "function",
"function": {
"name": t["name"],
"description": t.get("description", ""),
"parameters": t.get("parameters", {"type": "object", "properties": {}}),
},
}
for t in ld_tools
]
# Anthropic
anthropic_tools = [
{
"name": t["name"],
"description": t.get("description", ""),
"input_schema": t.get("parameters", {"type": "object", "properties": {}}),
}
for t in ld_tools
]
# Bedrock Converse
bedrock_tool_config = {
"tools": [
{
"toolSpec": {
"name": t["name"],
"description": t.get("description", ""),
"inputSchema": {"json": t.get("parameters", {"type": "object", "properties": {}})},
}
}
for t in ld_tools
]
}
# Gemini
gemini_tools = [
{
"function_declarations": [
{
"name": t["name"],
"description": t.get("description", ""),
"parameters": t.get("parameters", {"type": "object", "properties": {}}),
}
for t in ld_tools
]
}
] if ld_tools else []
An agent that uses tools runs a short loop: call the provider, dispatch any tool calls, loop again, stop when the provider returns a final answer. Three rules apply regardless of provider:
MAX_STEPS = 5 is a safe default. A runaway tool loop is almost always a prompt or schema bug, not a case that needs 50 iterations.tracker.track_tool_call(tool_name) / tracker.trackToolCall(toolName) for each tool the agent actually executes. This is what the Monitoring tab counts as tool usage.choice.finish_reason != "tool_calls"; Anthropic → response.stop_reason != "tool_use"; Bedrock Converse → response["stopReason"] != "tool_use"; Gemini → response.function_calls empty; OpenAI Responses API → no function_call items in response.output.Skeleton (Python, Anthropic — the other providers follow the same shape with their own stop-reason check and tool-result formatting):
messages = [{"role": "user", "content": initial_input}]
MAX_STEPS = 5
for _ in range(MAX_STEPS):
response = tracker.track_metrics_of(
anthropic_metrics,
lambda: anthropic_client.messages.create(
model=agent.model.name,
system=agent.instructions,
messages=messages,
tools=anthropic_tools,
**params,
),
)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
if block.name not in tool_handlers:
raise ValueError(f"Unknown tool: {block.name}")
result = tool_handlers[block.name](**block.input)
tracker.track_tool_call(block.name)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
Per-provider tool-call payload shapes live in the built-in-metrics references:
tool_use blocks and tool_result payloadstoolUse / toolResult Converse formatfunctionCalls / functionResponse partscreate_react_agentLangGraph, CrewAI, and AutoGen often generate schemas from function definitions. You still need to create tools in LaunchDarkly and attach keys to variations so the SDK knows what's available.
| Situation | Action |
|---|---|
| Tool already exists (409) | Use existing or create with different key |
| Schema invalid | Use raw JSON Schema format (type: object, properties, required) |
| Wrong endpoint assumed | The tools use /ai-tools, not /ai-configs/tools |
instructions, messages, model, or parameters into the tool-attachment PATCH. Send tools alone unless the user explicitly asked for a multi-field update — bundled PATCHes silently clobber UI edits to the other fields.configs-create -- Create config before attaching toolsconfigs-variations -- Manage variations with different tool sets