0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINE WORKS AiNote の議事録を Claude に読ませる MCP サーバーを作る

0
Last updated at Posted at 2026-09-23

はじめに

LINE WORKS には AI 議事録サービス「AiNote」があり、会議を録音すると文字起こしが作られ、テンプレートを選ぶと AI 要約も生成できます。2026 年 4 月のアップデートで外部連携 API が公開され、ノートの一覧・検索・取得がプログラムから行えるようになりました。

この API を MCP (Model Context Protocol) サーバーにして Claude に接続すると、次のような会話ができるようになります。

  • 「先週の定例で決まったことを教えて」
  • 「◯◯プロジェクトに関する会議を探して、参加者を一覧にして」
  • 「田中さんが発言した箇所だけ抜き出して」

Claude が自分で「ノートを検索する」「要約を取る」「必要なら文字起こしを読む」という手順を選び、答えを返してきます。

会議で決まったことを聞くと要約から回答が返る

質問文に「lineworks-ainote を使って」と付いているのは、撮影した環境に別の MCP サーバーも登録されていたためで、サーバーが 1 つだけの環境では不要です。

本記事では、この MCP サーバーを Python で 1 ファイルとして実装し、Claude Desktop と Claude Code から使えるようにするまでを説明します。コードは記事末尾のリポジトリにまとめてあります。

この記事で扱うこと

  • AiNote API の認証 (OAuth 2.0)
  • トークンの安全な保存
  • MCP ツールの設計 (要約と文字起こしの分離、docstring による誘導)
  • Claude Desktop / Claude Code への登録

前提

  • LINE WORKS のテナントで AiNote が使えること。API は公開当初 Enterprise プラン限定でしたが、2026 年 8 月のアップデートで Team / Business プランでも利用できるようになりました。Developer Console でスコープに ainote.read が選べれば利用できます
  • Developer Console でアプリを作成できること。一般メンバーにはこの権限がないことが多いので、管理者にアプリの作成か権限の付与を依頼する必要があります
  • Python 3.12 以上と uv が入っていること
  • Claude Desktop または Claude Code
  • 会議の文字起こしを外部の LLM に送ることになるので、社内の AI 利用ルールや情報の取り扱い区分を事前に確認してください

動作確認は Windows 11 で行いました。macOS はキーチェーンで動く設計ですが未確認です。Linux や WSL では keyring のバックエンド (Secret Service など) がないとエラーになります。keyrings.alt を入れると動きますが平文保存になるので、その場合は保存先の扱いに注意してください。

AiNote API の全体像

使う API は 3 つです。エンドポイントの詳細やレスポンスの構造、エラーの実例は別記事 LINE WORKS API AiNote の議事録 (文字起こし・要約) を取得する にまとめたので、ここでは MCP サーバーの設計に関わる点だけ書きます。

用途 エンドポイント
ノート一覧 GET /users/me/ainote/notes
ノート検索 GET /users/me/ainote/search?query=...
ノート取得 GET /users/me/ainote/notes/{noteId}

1. User Account 認証専用です。 ユーザーのノートを扱う API は Service Account 認証 (JWT) では呼べず、ユーザー本人がブラウザでログインして認可する OAuth 2.0 認可コードフローが必須です。MCP サーバーにこのフローを組み込む必要があります。

2. 読み取りだけなら ainote.read で足ります。 削除は実装しないので、最小権限のスコープを使います。

3. 検索だけレート制限が厳しいです。 通常の API はドメインあたり 240 requests/min ですが、ノート検索は 60 requests/min です。Claude が言い換えを繰り返して検索を連打すると簡単に超えるので、ツール側で呼び出し間隔を 1 秒以上あけ、docstring でも「同じ意図で言い換え検索を繰り返さない」と伝えます。それでも 429 が返ったときは、待つよう促すメッセージを Claude に返します。

設計方針

実装に入る前に、この MCP サーバーの設計で重視した 3 点を書いておきます。ここが本記事で一番伝えたい部分です。

要約と文字起こしを別のツールにする

ノート取得 API のレスポンスは、AI 要約 (summaries) と文字起こし全文 (scripts) の両方を含みます。手元で測ると 5 分 8 秒の会議で全体が約 11KB、うち文字起こしが 7.9KB、要約は 2.9KB でした。文字起こしは録音時間に比例して増えるので、1 時間の会議なら 100KB 近くになります。

これを「ノートの内容を取る」という 1 つのツールで返してしまうと、Claude が要約だけ知りたいときにも全文を受け取ることになり、トークンを浪費するうえ、応答も遅くなります。そこで次の 4 つに分けました。

ツール 返すもの 返さないもの
list_notes タイトル・作成日時・音声の長さ 要約・文字起こし
search_notes 同上 同上
get_note_summary AI 要約・参加者・文字起こしのブロック数 文字起こし本文
get_note_transcript [時刻] 話者: 発言 に整形した全文。max_blocks で上限指定可 (なし)

一覧と検索は「どのノートか」を特定するためだけの情報を返し、内容は一切含めません。内容を知るにはまず要約、足りなければ全文、という段階構造です。

docstring で Claude を誘導する

MCP ではツールの docstring がそのまま Claude への説明になります。上の段階構造を Claude に守らせるには、docstring に書いておくのが一番確実です。

@server.tool()
def get_note_summary(note_id: str) -> dict:
    """ノートの AI 要約と参加者を取得する。文字起こし全文は含まない。

    ノートの内容を知りたい場合はまずこれを使う。要約で足りない場合にのみ
    get_note_transcript で全文を取得する。
    """

こう書いておくと、Claude は「まず要約を見て、必要なら全文」という手順を自然に選びます。何も書かなければ、Claude はツール名と引数だけを手がかりに選ぶことになり、どちらを先に呼ぶかは運任せになります。実際に誘導が効いているかは「使ってみる」の節で確認します。

トークンをファイルに書かない

MCP サーバーはローカルで動くので、Refresh Token をどこかに保存する必要があります。よく見るのはホームディレクトリの JSON ファイルに平文で書く方法ですが、90 日有効なトークンを平文で置くのは避けたいところです。

今回は keyring ライブラリを使い、Windows なら資格情報マネージャー、macOS ならキーチェーンに保存します。Client ID / Client Secret も同じ場所に入れるので、設定ファイルや環境変数にシークレットを一切書かずに済みます。

これには副次的な利点もあります。Claude Desktop は Windows では MSIX パッケージアプリとして動いており、MCP サーバーの子プロセスにユーザー環境変数が渡らないことがあります。環境変数に依存しない設計にしておくと、この問題を最初から回避できます。

実装

ファイルは ainote_mcp.py の 1 つだけです。先頭に PEP 723 のインラインメタデータで依存関係を書いておくと、uv run が仮想環境の作成からパッケージの解決まで自動でやってくれます。

記事に載せるコードは要点が見えるように簡略化しています (エラー分岐やタイムアウト、入力チェックの一部を省いています)。完全版は末尾のリポジトリを参照してください。

# /// script
# requires-python = ">=3.12"
# dependencies = ["mcp>=2,<3", "httpx>=0.27", "keyring>=25"]
# ///

定数

AUTH_URL = "https://auth.worksmobile.com/oauth2/v2.0/authorize"
TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token"
API_BASE = "https://www.worksapis.com/v1.0"
SCOPE = "ainote.read"

REDIRECT_PORT = 8765
REDIRECT_URI = f"http://localhost:{REDIRECT_PORT}/callback"

KEYRING_SERVICE = "lineworks-ainote-mcp"

REDIRECT_URI は後で Developer Console に登録するものと完全一致させる必要があります。末尾のスラッシュ 1 つ違っても認可が失敗します。

keyring への読み書き

import keyring

def _store(key: str, value: str) -> None:
    keyring.set_password(KEYRING_SERVICE, key, value)

def _load(key: str) -> str | None:
    return keyring.get_password(KEYRING_SERVICE, key)

def _save_tokens(tokens: dict) -> None:
    _store("access_token", tokens["access_token"])
    _store("expires_at", str(time.time() + int(tokens.get("expires_in", 3600))))
    if tokens.get("refresh_token"):
        # リフレッシュ時に新しい refresh_token が返ることがある (Refresh Token Rotation)
        _store("refresh_token", tokens["refresh_token"])

保存するのは Client ID、Client Secret、Access Token、その期限、Refresh Token の 5 つです。リフレッシュの応答に refresh_token が含まれていたら上書きします。認証のドキュメントに「Refresh Token Rotation が ON の場合、新しい Refresh Token も発行される」とあるためです。

なお Windows の資格情報マネージャーは 1 エントリあたり 2,560 バイトの上限があります。トークンが長い場合に備えて、実際のコードでは _store / _load が値を 1,000 文字ごとに分割して保存しています (記事では省略しています)。

初回ログイン (認可コードフロー)

MCP サーバーは Claude から stdio で起動されるので、その中でブラウザを開いてログインさせるのは扱いにくいです。初回ログインはターミナルから明示的に実行する auth サブコマンドに分けました。

def login() -> None:
    client_id = input("Client ID: ").strip()
    client_secret = getpass.getpass("Client Secret: ")

    state = secrets.token_urlsafe(16)
    url = AUTH_URL + "?" + urlencode({
        "client_id": client_id,
        "redirect_uri": REDIRECT_URI,
        "response_type": "code",
        "scope": SCOPE,
        "state": state,
    })

    code = _receive_code(url, state)
    tokens = _post_token(client_id, client_secret, {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
    })

    _store("client_id", client_id)
    _store("client_secret", client_secret)
    _save_tokens(tokens)
    print("認証が完了しました。トークンは OS の資格情報ストアに保存されています。")

_receive_code はローカルに一時的な HTTP サーバーを立て、ブラウザを開き、リダイレクトで返ってくる認可コードを 1 回だけ受け取ります。

def _receive_code(auth_url: str, expected_state: str) -> str:
    result: dict[str, str | None] = {}

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            q = parse_qs(urlparse(self.path).query)
            result["code"] = (q.get("code") or [None])[0]
            result["state"] = (q.get("state") or [None])[0]
            result["error"] = (q.get("error") or [None])[0]
            result["error_description"] = (q.get("error_description") or [None])[0]
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write("認証が完了しました。このタブは閉じてください。".encode())

        def log_message(self, *_):  # 標準エラーへのアクセスログを止める
            pass

    with HTTPServer(("localhost", REDIRECT_PORT), Handler) as srv:
        webbrowser.open(auth_url)
        srv.handle_request()          # 1 リクエストだけ処理して終了

    if result.get("error"):
        raise SystemExit(f"認可が拒否されました: {result['error']} {result.get('error_description') or ''}")
    if result.get("state") != expected_state:
        raise SystemExit("state が一致しません。認可をやり直してください。")
    if not result.get("code"):
        raise SystemExit("認可コードが取得できませんでした。")
    return result["code"]

サーバーを bind してからブラウザを開く順序が重要です。逆にすると、リダイレクトが先に届いて接続拒否になることがあります。ユーザーが認可画面で拒否した場合は code ではなく errorerror_description が返ってくるので、それも拾って表示します。

トークンエンドポイントへの POST は application/x-www-form-urlencoded です。httpxdata= で送れば自動でその形式になります。

def _post_token(client_id: str, client_secret: str, params: dict) -> dict:
    data = {"client_id": client_id, "client_secret": client_secret, **params}
    res = httpx.post(TOKEN_URL, data=data, timeout=30.0)
    if res.status_code >= 400:
        raise ToolError(f"トークン取得に失敗しました: HTTP {res.status_code} {res.text[:300]}")
    return res.json()

Access Token の自動更新

MCP サーバー側は、保存済みの Access Token が有効ならそれを使い、期限が近ければ Refresh Token で更新します。Access Token の有効期限は 1 時間または 24 時間で、トークン取得時のレスポンスの expires_in に入っています。Refresh Token は 90 日です。

def get_access_token() -> str:
    token, expires_at = _load("access_token"), _load("expires_at")
    if token and expires_at and time.time() < float(expires_at) - 60:
        return token

    refresh = _load("refresh_token")
    client_id, client_secret = _load("client_id"), _load("client_secret")
    if not (refresh and client_id and client_secret):
        raise ToolError(
            "未認証です。ターミナルで `uv run ainote_mcp.py auth` を実行してください。"
        )
    tokens = _post_token(client_id, client_secret, {
        "grant_type": "refresh_token",
        "refresh_token": refresh,
    })
    _save_tokens(tokens)
    return tokens["access_token"]

Refresh Token も切れていた場合 (90 日以上使わなかった場合) は _post_token がエラーを投げるので、auth をやり直してもらいます。

API 呼び出しとエラーの整形

def api_get(path: str, params: dict | None = None) -> dict:
    clean = {k: v for k, v in (params or {}).items() if v is not None}
    headers = {"Authorization": f"Bearer {get_access_token()}"}
    try:
        res = httpx.get(f"{API_BASE}/{path}", headers=headers, params=clean, timeout=30.0)
    except httpx.RequestError as exc:
        raise ToolError(f"LINE WORKS API に接続できません ({exc.__class__.__name__})") from exc

    if res.status_code >= 400:
        detail = ""
        try:
            body = res.json()
            detail = f" [{body.get('code')}: {body.get('description')}]"
        except ValueError:
            pass
        if res.status_code == 429:
            raise ToolError(f"HTTP 429{detail} 呼び出し上限です。1 分ほど待ってください。")
        raise ToolError(f"HTTP {res.status_code}{detail} -> {path}")
    return res.json()

2 つポイントがあります。

  • 想定したエラーは ToolError で投げる。 MCP SDK 2.x では ToolError のメッセージだけがクライアント (Claude) に届きます。それ以外の例外は Error executing tool xxx という情報のないエラーにまとめられてしまい、原因が分からなくなります
  • API が返す code / description を必ず添える。 403 はスコープ不足でも権限不足でも返るので、API 自身の説明がないと切り分けができません

ツール定義

ここからが MCP サーバー本体です。MCPServer を作り、関数に @server.tool() を付けるだけでツールになります。型ヒントと docstring からスキーマが自動生成されます。

from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

server = MCPServer("lineworks-ainote")

def _hms(milliseconds) -> str:
    """ミリ秒を H:MM:SS にする。audioDuration と発言の offset はミリ秒。"""
    try:
        total = int(milliseconds) // 1000
    except (TypeError, ValueError):
        return ""
    h, rem = divmod(total, 3600)
    m, s = divmod(rem, 60)
    return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"

def _summarize(note: dict) -> dict:
    """一覧用。要約も文字起こしも含めない。"""
    return {
        "noteId": note.get("noteId"),
        "title": note.get("title"),
        "createdTime": note.get("createdTime"),
        "duration": _hms(note.get("audioDuration")),
        "language": note.get("recognitionLanguage"),
    }

def _note_list(data: dict) -> dict:
    notes = data.get("notes") or []
    return {
        "count": len(notes),
        "notes": [_summarize(n) for n in notes],
        "nextCursor": (data.get("responseMetaData") or {}).get("nextCursor"),
    }
@server.tool()
def list_notes(count: int = 20, cursor: str | None = None) -> dict:
    """AiNote のノート一覧を新しい順に取得する。要約も文字起こしも含まない。

    タイトル・作成日時・音声の長さのみを返す。内容を読むには、
    ここで得た noteId を get_note_summary に渡す。

    Args:
        count: 取得件数。既定 20。
        cursor: 前回の応答の nextCursor。次ページを取得する場合に指定する。
    """
    return _note_list(api_get("users/me/ainote/notes", {"count": count, "cursor": cursor}))


@server.tool()
def search_notes(query: str, count: int = 20, cursor: str | None = None) -> dict:
    """AiNote のノートをキーワードで検索する。要約も文字起こしも含まない。

    検索 API の呼び出し上限は 60 回/分と他より厳しい (ツール側で 1 秒間隔に制限する)。
    同じ意図でキーワードを言い換えて繰り返し検索しないこと。
    件数が足りない場合は cursor でページを進める。

    Args:
        query: 検索キーワード (必須)。
        count: 取得件数。既定 20。
        cursor: 前回の応答の nextCursor。
    """
    if not query or not query.strip():
        raise ToolError("query は必須です。検索キーワードを指定してください。")
    _throttle_search()   # 直前の検索から 1 秒未満なら待つ (実装は省略)
    return _note_list(
        api_get("users/me/ainote/search", {"query": query, "count": count, "cursor": cursor})
    )


@server.tool()
def get_note_summary(note_id: str) -> dict:
    """ノートの AI 要約と参加者を取得する。文字起こし全文は含まない。

    ノートの内容を知りたい場合はまずこれを使う。要約で足りない場合にのみ
    get_note_transcript で全文を取得する。

    要約はノートの作成者が AiNote 上でテンプレートを選んで生成するもの。
    summaries が空の場合は作成者が要約を生成していないので、内容を知るには
    get_note_transcript で全文を読む。

    Args:
        note_id: ノートID。list_notes または search_notes の結果から得る。
    """
    note = api_get(f"users/me/ainote/notes/{note_id}")
    return {
        "noteId": note.get("noteId"),
        "title": note.get("title"),
        "createdTime": note.get("createdTime"),
        "duration": _hms(note.get("audioDuration")),
        "attendees": [
            a.get("attendeeName") for a in (note.get("attendees") or []) if a.get("attendeeName")
        ],
        "summaries": [
            {
                "summaryType": s.get("summaryType"),
                "summaryName": s.get("summaryName"),
                "content": s.get("content"),
            }
            for s in (note.get("summaries") or [])
        ],
        "transcriptBlocks": len(note.get("scripts") or []),
    }


@server.tool()
def get_note_transcript(note_id: str, max_blocks: int | None = None) -> dict:
    """ノートの文字起こし全文を取得する。

    1 時間の会議で数十 KB になる。要約で足りる場合は get_note_summary を使うこと。
    発言は `[時刻] 話者: 発言` の形に整形して返す。

    Args:
        note_id: ノートID。
        max_blocks: 返す発言ブロックの上限。省略すると全件。
    """
    note = api_get(f"users/me/ainote/notes/{note_id}")
    scripts = note.get("scripts") or []
    total = len(scripts)
    if max_blocks and max_blocks > 0:
        scripts = scripts[:max_blocks]

    lines = [
        f"[{_hms(b.get('startOffset'))}] {b.get('attendeeName') or '話者不明'}: "
        f"{(b.get('text') or '').strip()}"
        for b in scripts
    ]
    return {
        "noteId": note.get("noteId"),
        "title": note.get("title"),
        "duration": _hms(note.get("audioDuration")),
        "totalBlocks": total,
        "returnedBlocks": len(scripts),
        "truncated": len(scripts) < total,
        "transcript": "\n".join(lines),
    }

get_note_summary が文字起こしのブロック数 (transcriptBlocks) を返しているのは意図的です。Claude はこの数を見て「全文を取るべきか、取るなら max_blocks をいくつにするか」を判断できます。

エントリポイント

def main() -> None:
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    if cmd == "auth":
        login()
    elif cmd == "logout":
        logout()      # 保存した資格情報をすべて削除
    else:
        server.run()  # stdio で待ち受ける

if __name__ == "__main__":
    main()

logout は保存済みの Client ID / Secret / トークンを削除するだけの小さなコマンドですが、別のテナントに切り替えるときや PC を手放すときに必要になるので付けています。

実装で踏んだ細かい点

API の癖 (時間がミリ秒単位、summary ではなく summaries を見る、query 必須、{userId}me が使える、など) は API の記事の「注意事項」にまとめています。MCP サーバーの設計に影響した点だけ挙げます。

  • AI 要約は自動では付きません。 ノートの作成者が AiNote 上でテンプレートを選んで生成したときだけ summaries に入ります。生成していないノートでは空配列になるので、get_note_summary の docstring に「summaries が空なら作成者が要約を生成していないので get_note_transcript で全文を読む」と書き、Claude が「要約がない」で止まらないようにしています
  • 発言ブロックの attendeeId は null のことがあります。 話者の区別には attendeeName (「参加者 1」など) を使い、それもなければ「話者不明」と表示します

Developer Console でのアプリ作成

  1. LINE WORKS Developer Console にログインし、「アプリの新規追加」でアプリを作ります
  2. OAuth Scopesainote.read を選びます
  3. Redirect URLhttp://localhost:8765/callback を登録します。コードの REDIRECT_URI と一字一句合わせてください
  4. 表示された Client IDClient Secret を控えます。この 2 つは次の auth コマンドで入力し、資格情報ストアに保存されます

Developer Console で ainote.read を選択

ダイアログの説明にあるとおり、契約プランによっては一部の Scope が表示されません。ここに ainote.read が出てこない場合は、そのテナントでは AiNote API が使えないということです。

初回ログイン

ターミナルで次を実行します。

uv run ainote_mcp.py auth

auth コマンドの実行画面

Client ID と Client Secret を聞かれるので入力すると、ブラウザが開いて LINE WORKS のログイン画面になります。ログインして許可すると、ブラウザに「認証が完了しました」と表示され、ターミナルにも次のように出ます。

認証が完了しました。トークンは OS の資格情報ストアに保存されています。
付与されたスコープ: ainote.read

Secret は getpass で読んでいるので画面に表示されません。Client ID は表示されるので、スクリーンショットを共有するときは注意してください。

これ以降、トークンは OS の資格情報ストアにあります。Windows なら「資格情報マネージャー」の「Windows 資格情報」に キー名.番号@lineworks-ainote-mcp という形のエントリが並んでいるはずです。

資格情報マネージャーに保存されたエントリ

client_id.n が分割数、client_id.0 が 1 番目の断片です。1 つだけ lineworks-ainote-mcp とサービス名そのままのエントリがありますが、これは keyring ライブラリの仕様で、最後に書き込んだ値がこの名前で保存されます。読み出し時は両方を探すので動作に影響はありません。

なお実際の Refresh Token は 230 文字程度で、資格情報マネージャーの上限には余裕があります。分割保存は保険として入れているだけで、通常は断片が 1 つ (.0) だけになります。

資格情報マネージャーを開くには、ターミナルか Win + R で次を実行します。

control /name Microsoft.CredentialManager

Claude への登録

Claude Desktop

claude_desktop_config.json に次を追加します。Windows では Claude Desktop が uv を PATH から解決できないことがあるので、command にはフルパスを書きます。パスは where.exe uv で確認できます (公式インストーラなら %USERPROFILE%\.local\bin\uv.exe、winget なら %LOCALAPPDATA%\Microsoft\WinGet\Packages\...\uv.exe です)。

{
  "mcpServers": {
    "lineworks-ainote": {
      "command": "C:\\Users\\<you>\\.local\\bin\\uv.exe",
      "args": ["run", "--quiet", "C:\\path\\to\\ainote_mcp.py"]
    }
  }
}

--quietuv の進捗表示を抑えるためのものです。

Claude Desktop を完全に終了して再起動すると、ツール一覧に lineworks-ainote が現れます。

Claude Code

1 行です。

claude mcp add lineworks-ainote -- uv run --quiet /path/to/ainote_mcp.py

既定では現在のプロジェクトだけに登録されます。どのプロジェクトからも使いたい場合は -s user を付けます。

使ってみる

docstring での誘導が本当に効いているかを確かめるため、Claude Code をヘッドレスモード (claude -p) で起動し、性質の違う 3 つの質問を投げて、呼ばれたツールの順序を記録しました。

質問 呼ばれたツール
「AiNote にある会議を一覧にして」 list_notes
「一番新しい会議で決まったことを教えて」 list_notesget_note_summary
「一番新しい会議で、最初の発言者が最初に話した内容を 1 文で教えて」 list_notesget_note_transcript

2 つ目の質問では要約だけで答えを組み立て、全文を取りに行っていません (冒頭の図がこのときの画面です)。3 つ目は発言そのものを問う質問なので、要約を経由せずに全文へ直行しています。各質問は 1 回ずつしか試していないので「常にこう動く」とは言えませんが、少なくともこの 3 ケースでは docstring に書いた「まず要約、足りなければ全文」の方針どおりに動きました。

3 つ目の質問で Claude は get_note_transcriptmax_blocks=2 で呼んでいます。「最初の発言者の最初の発言」という質問から先頭 2 ブロックで足りると判断した値です。呼び出し後は応答の totalBlockstruncated を見て、「全 25 ブロックのうち残りは読み込んでいない」と読み残しがあることを自分で説明しています。

get_note_transcript を max_blocks=2 で呼んでいる画面

まとめ

  • AiNote API は User Account 認証専用なので、認可コードフローの実装が必須
  • トークンは keyring で OS の資格情報ストアに置き、設定ファイルにシークレットを書かない
  • 一覧・要約・全文をツールとして分け、docstring で「まず要約」と誘導することで、トークン消費と応答速度を両立する
  • 想定エラーは ToolError で投げ、API の code / description を添える

コード全体は以下のリポジトリにあります。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?