איך עובדת הזדהות OAuth ב MCP של Langlets
בוובינר היום ראינו איך לחבר סוכני AI למערכות שלנו ודיברנו על הדוגמה של Langlets ומימוש ה MCP שם. מפאת קוצר זמן דילגתי על הירידה לפרטים התחביריים של הקוד. בפוסט זה אני אתחיל לסגור את הפער ואציג את הפרטים הטכניים סביב הזדהות OAuth.
1. רישום האפליקציה
בשלב הראשון בתהליך ההזדהות סוכן ה AI צריך להירשם בתור "אפליקציה מורשית גישה". בדרך כלל ב OAuth אנחנו רושמים אפליקציות צד שלישי שיוכלו להתחבר למערכות שלנו בצורה ידנית, לדוגמה אם אתם רוצים לכתוב אפליקציה שתיגש בשם המשתמש לגיטהאב עליכם להיכנס למסך הניהול של גיטהאב ולרשום שם "אפליקציית גיטהאב חדשה" בחשבון שלכם. אתם תקבלו מזהה של האפליקציה בדמות Client Id ו Client Secret ותשתמשו במזהה זה כשתשלחו את המשתמשים להזדהות מול גיטהאב עבור האפליקציה שלכם.
בעולם של MCP סוכני AI לא יכולים לרשום את עצמם מראש מול האפליקציה שלי ולכן OAuth מאפשר מנגנון של רישום אוטומטי. ב langlets מנגנון זה ממומש בקבצים:
https://github.com/ynonp/langlets-rails/blob/main/app/controllers/wellknowncontroller.rb
https://github.com/ynonp/langlets-rails/blob/main/app/controllers/oauth/registrations_controller.rb
סוכן ה AI מתחיל עם הנתיב oauth_authorization_server ודרכו מקבל את השמות של כל הנתיבים שהוא צריך כדי לרשום אפליקציה. לאחר מכן הוא ממשיך לנתיב ההרשמה ושם מגיע לפונקציה RegistrationsController#create. פונקציה זו יוצרת אפליקציה חדשה ב Doorkeeper שזו הספריה ברובי שאחראית על הזדהות באמצעות טוקנים. זה הקוד הרלוונטי ממנה:
application = Doorkeeper::Application.new(
name: params[:client_name].presence || "Unnamed agent client",
redirect_uri: redirect_uris.join("\n"),
scopes: allowed_scopes(params[:scope]),
confidential: params[:token_endpoint_auth_method] != "none",
dynamically_registered: true
)
unless application.save
return rfc_error("invalid_client_metadata", application.errors.full_messages.join("; "))
end
render status: :created, json: registration_response(application, redirect_uris)
רק אחרי יצירת האפליקציה אפשר לשלוח את המשתמש לתהליך ההזדהות ולקבל טוקן עבורו.
2. הזדהות בשם המשתמש ובקשת קוד גישה
אחרי שסוכן ה AI נרשם בתור אפליקציה הוא מקבל תשובה במבנה הבא (עדיין מתוך הקובץ registrations_controller.rb):
response = {
client_id: application.uid,
client_id_issued_at: application.created_at.to_i,
client_name: application.name,
redirect_uris: redirect_uris,
grant_types: %w[authorization_code refresh_token],
response_types: %w[code],
token_endpoint_auth_method: application.confidential? ? "client_secret_basic" : "none",
scope: application.scopes.to_s
}
אנחנו מזהים שם את מזהה האפליקציה client_id. בשלב הבא סוכן ה AI שולח את הגולש באמצעות הפניית הדפדפן לנתיב הזדהות על Langlets:
Open /oauth/authorize?client_id=...&code_challenge=...&code_challenge_method=S256
קריאה זו מטופלת בצד של langlets על ידי ספריית Doorkeeper בזכות השורות האלה בקובץ config/routes.rb:
use_doorkeeper do
skip_controllers :authorized_applications
end
דורקיפר מטפלת בפרמטרים שהתקבלו ומציגה טופס הזדהות שמוגדר בקובץ:
https://github.com/ynonp/langlets-rails/blob/main/app/views/doorkeeper/authorizations/new.html.erb
בבסיס הנתונים דורקיפר שומרת את פרמטר ה Code Challenge בטבלת oauth_access_grants. זו הגדרת הטבלה:
create_table "oauth_access_grants", force: :cascade do |t|
t.bigint "resource_owner_id", null: false
t.bigint "application_id", null: false
t.string "token", null: false
t.integer "expires_in", null: false
t.text "redirect_uri", null: false
t.string "scopes", default: "", null: false
t.datetime "created_at", null: false
t.datetime "revoked_at"
t.string "code_challenge"
t.string "code_challenge_method"
t.index ["application_id"], name: "index_oauth_access_grants_on_application_id"
t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true
end
אנחנו עוד נצטרך את הערך שלו. אחרי שהמשתמש מאשר את הגישה דורקיפר שולחת את המשתמש חזרה לסוכן ה AI עם פרמטר בשם code.
3. החלפת קוד הגישה בטוקן
סוכן ה AI קיבל קוד גישה אבל עדיין אי אפשר לבצע פעולות במערכת עם קוד זה, ל OAuth יש עוד שלב אחד - החלפת קוד הגישה בטוקן. בשביל ההחלפה סוכן ה AI פונה לנתיב:
POST /oauth/token {grant_type, code, code_verifier, client_id}
ומצרף את הקוד שקיבל. דורקיפר בצד השרת מבצעת:
מוצאת את השורה הרלוונטית מ
oauth_access_grantsבה עמודת token שווה לקוד שהתקבל כפרמטר.מחשבת Hash של
code_verifierומוודאת שהוא תואם לcode_challengeששמור בטבלה.במידה והכל תקין מחזירה Access Token ו Refresh Token. ה Access Token תקף לשעתיים וה Refresh Token לחודש. בעזרת ה Refresh Token אפשר לקבל Access Tokens נוספים בהמשך.
4. ביצוע בקשות מול ה MCP
עכשיו יש לסוכן ה AI אסימון גישה והוא יכול להתחיל לבצע פעולות בשם המשתמש. פניה ראשונה ל /mcp מגיעה לקובץ:
https://github.com/ynonp/langlets-rails/blob/main/app/controllers/mcp_controller.rb
שם אנחנו פוגשים את הפונקציה handle שמגדירה את הכלים הזמינים ומפעילה את הכלי המתאים:
def handle
server = MCP::Server.new(
name: "langlets",
version: "1.0.0",
tools: [ Tools::GetVocabulary, Tools::GetCourses ],
server_context: { user: @current_user, scopes: @token_scopes, base_url: request.base_url }
)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
server, stateless: true, enable_json_response: true
)
status, transport_headers, body = transport.handle_request(request)
transport_headers.each { |key, value| response.set_header(key, value) }
render body: body.join, status: status, content_type: transport_headers["Content-Type"] || "application/json"
end
הכלים עצמם מוגדרים בקבצים בתיקיה:
https://github.com/ynonp/langlets-rails/tree/main/app/mcp/tools
ובתוך כל כלי המשתנה server_context[:user] מחזיק את המשתמש המחובר.
אומנם לנגלטס כתוב ב Ruby אבל העקרונות וזרימת המידע שתוארה כאן יעבדו בכל מערכת שתרצו לחבר לסוכני AI או אפילו באופן כללי יותר למערכות צד שלישי באמצעות OAuth.