Guides
ツール開発ガイド
Python/TypeScriptでツールを開発する方法
概念
Jinba Tool Registryでは、以下の3つの概念でツールを管理します。
Organization
└── ToolSet(ツールのグループ)
├── Tool A
├── Tool B
└── Tool C- ToolSet: 関連するToolをグループ化するコンテナ。サンドボックス設定(言語、パッケージ、リソース)はToolSet単位で共有されます
- Tool: 単一の処理単位。Input/Outputスキーマとコードで定義されます
- Version: ToolSetのimmutableなスナップショット。Publishすることで実行可能になります
Pythonでのツール開発
基本構造
Pydantic BaseModelでInput/Outputを定義し、run()関数を実装します。
from pydantic import BaseModel, Field
class Input(BaseModel):
name: str = Field(description="挨拶する相手の名前")
class Output(BaseModel):
message: str = Field(description="挨拶メッセージ")
def run(input: Input) -> Output:
return Output(message=f"Hello, {input.name}!")環境変数の利用
ToolSetに設定した環境変数はos.environからアクセスできます。
import os
from pydantic import BaseModel
class Input(BaseModel):
channel: str
class Output(BaseModel):
success: bool
def run(input: Input) -> Output:
api_key = os.environ["SLACK_API_KEY"]
# api_key を使って処理...
return Output(success=True)環境変数の設定方法は環境変数を参照してください。
外部ライブラリの利用
ToolSetのサンドボックス設定でパッケージを指定します。バージョンを指定するとそのバージョンがインストールされます。
import requests
from pydantic import BaseModel
class Input(BaseModel):
url: str
class Output(BaseModel):
status_code: int
body: str
def run(input: Input) -> Output:
response = requests.get(input.url)
return Output(status_code=response.status_code, body=response.text[:1000])エラーハンドリング
例外を送出すると、エラー情報がRunResultに記録されます。
from pydantic import BaseModel
class Input(BaseModel):
value: int
class Output(BaseModel):
result: int
def run(input: Input) -> Output:
if input.value < 0:
raise ValueError("value must be non-negative")
return Output(result=input.value * 2)複雑な型の定義
ネストしたモデルやOptionalフィールドも使用できます。
from typing import Optional
from pydantic import BaseModel, Field
class Address(BaseModel):
city: str
country: str = "JP"
class Input(BaseModel):
name: str
age: Optional[int] = None
address: Address
tags: list[str] = Field(default_factory=list)
class Output(BaseModel):
summary: str
def run(input: Input) -> Output:
parts = [f"Name: {input.name}"]
if input.age:
parts.append(f"Age: {input.age}")
parts.append(f"Location: {input.address.city}, {input.address.country}")
return Output(summary=", ".join(parts))TypeScriptでのツール開発
基本構造
Zodでスキーマを定義し、run()関数をエクスポートします。
import { z } from "zod";
export const input = z.object({
name: z.string().describe("挨拶する相手の名前"),
});
export const output = z.object({
message: z.string().describe("挨拶メッセージ"),
});
export async function run(data: z.infer<typeof input>) {
return { message: `Hello, ${data.name}!` };
}環境変数の利用
ToolSetに設定した環境変数はprocess.envからアクセスできます。
import { z } from "zod";
export const input = z.object({
channel: z.string(),
});
export const output = z.object({
success: z.boolean(),
});
export async function run(data: z.infer<typeof input>) {
const apiKey = process.env.SLACK_API_KEY;
// apiKey を使って処理...
return { success: true };
}外部パッケージの利用
ToolSetのサンドボックス設定でパッケージを指定します。
import { z } from "zod";
import axios from "axios";
export const input = z.object({
url: z.string().url(),
});
export const output = z.object({
status: z.number(),
data: z.unknown(),
});
export async function run(data: z.infer<typeof input>) {
const response = await axios.get(data.url);
return { status: response.status, data: response.data };
}エラーハンドリング
import { z } from "zod";
export const input = z.object({
value: z.number(),
});
export const output = z.object({
result: z.number(),
});
export async function run(data: z.infer<typeof input>) {
if (data.value < 0) {
throw new Error("value must be non-negative");
}
return { result: data.value * 2 };
}高度なパターン
HTTP APIの呼び出し
import os
import requests
from pydantic import BaseModel
class Input(BaseModel):
query: str
class Output(BaseModel):
results: list[dict]
def run(input: Input) -> Output:
response = requests.get(
"https://api.example.com/search",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
params={"q": input.query},
)
response.raise_for_status()
return Output(results=response.json()["results"])ファイル処理
import csv
import io
from pydantic import BaseModel
class Input(BaseModel):
csv_content: str
class Output(BaseModel):
rows: int
headers: list[str]
def run(input: Input) -> Output:
reader = csv.reader(io.StringIO(input.csv_content))
headers = next(reader)
rows = sum(1 for _ in reader)
return Output(rows=rows, headers=headers)テスト
Web UIでテスト
ToolSetの詳細画面で各Toolの「Test」ボタンをクリックし、入力値を指定してテスト実行できます。Publishせずにドラフト状態のコードをテストできます。
SDKでテスト
const result = await client.test("my-toolset", "my-tool", {
name: "World",
});
console.log(result.output); // { message: "Hello, World!" }
console.log(result.logs); // { stdout: [...], stderr: [...] }REST APIでテスト
curl -X POST https://tool-registry-api.jinba.io/v1/orgs/{orgId}/toolsets/{slug}/tools/{toolSlug}/test \
-H "Authorization: Bearer $JINBA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"name": "World"}}'ベストプラクティス
スキーマにdescriptionを付与する
スキーマフィールドにdescriptionを付与すると、MCPクライアントやドキュメントで表示されます。
class Input(BaseModel):
query: str = Field(description="検索クエリ")
limit: int = Field(default=10, description="最大取得件数")export const input = z.object({
query: z.string().describe("検索クエリ"),
limit: z.number().default(10).describe("最大取得件数"),
});entrypointのカスタマイズ
デフォルトのエントリポイントはrunですが、Toolの設定で変更できます。
def execute(input: Input) -> Output: # entrypoint: "execute"
return Output(result=input.value)ToolSetの粒度設計
- 同じサンドボックス設定(言語、パッケージ)を共有するToolをまとめる
- 関連する機能(例: Slack操作、データ変換)をグループ化する
- 独立したライフサイクルを持つToolは別のToolSetに分ける