Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring RuboCop with MCP

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Exploring RuboCop with MCP

Avatar for Koichi ITO

Koichi ITO

March 24, 2026

More Decks by Koichi ITO

Other Decks in Programming

Transcript

  1. ಺ఆऀ *ODPNJOH "EWJTPS "EWJTPS 1FPQMF"TTPDJBUFEXJUI&4. *OD 4 Q FBLFS -5

    !LPJD !GVHBLLCO !OTHD !NBJNVYY !LBTVNJQPO !XBJEPJ !IBNDBQ !N@QJYZ " UUFOEFF " UUFOEFF " UUFOEFF " UUFOEFF " UUFOEFF " UUFOEFF " UUFOEFF !4)(".&-*/,4 !IBSVHVDIJZVNB !KVOL " UUFOEFF " UUFOEFF 4 Q FBLFS " UUFOEFF " UUFOEFF " UUFOEFF !NON !ZVDBPIPVST !XBUBSP 5#" !LBLVUBOJ !BNBUTVEB " UUFOEFF 0 SHBOJ[FS " UUFOEFF !GLJOP 0 SHBOJ[FS "DPNNVOJUZ GPSCVJMEFST :PVBSF XFMDPNFIFSF
  2. "/FX6TFS w )VNBOT $-* &EJUPST*%&T  w 1SPHSBNT -41 *OUFHSBUJPOT

     w "HFOUT "DSPTT"MM </&8> Exploring the AI ​ ​ Era
  3. 5PPM*OWPDBUJPOGSPN"*"HFOUT w #BTI $-* rubocopDPNNBOE w .$11SPUPDPMCBTFE.$1DBMMT w 4LJMMT/BUVSBMMBOHVBHF MFTT3VCZ,BJHJMJLF

    w -413VCZ-41JOUFHSBUJPO w "OFXBQQSPBDIZFUUPDPNF🤔 IUUQTHJUIVCDPNBOUISPQJDTDMBVEFQMVHJOTP ffi DJBMQVMM
  4. 5PPM*OWPDBUJPOGSPN"*"HFOUT w #BTI $-* rubocopDPNNBOE w .$1-BZJOHUIFTDB ff PMEJOH fi

    STU w 4LJMMT/BUVSBMMBOHVBHF MFTT3VCZ,BJHJMJLF w -413VCZ-41JOUFHSBUJPO w "OFXBQQSPBDIZFUUPDPNF🤔 IUUQTHJUIVCDPNBOUISPQJDTDMBVEFQMVHJOTP ffi DJBMQVMM
  5. 0G fi DJBM.$14%,T w -JDFOTFEVOEFS"QBDIF USBOTJUJPOJOHGSPN.*5  HPWFSOFECZUIF"HFOUJD"*'PVOEBUJPO -JOVY'PVOEBUJPO 

    w 1SPWJEFT4%,TGPSCVJMEJOHCPUI.$1 TFSWFSTBOEDMJFOUT w (PWFSOFEVOEFSUIF4%,5JFSJOH4ZTUFN 
  6. $POGPSNBODF5FTU w 0 ffi DJBMUFTUTVJUFUIBU4%,TNVTUQBTT w -JLFSVCZTQFD CVUGPS.$1 w 3VCZ4%,TFSWFS

    DMJFOU BDIJFWFE 5JFSSFRVJSFTPOCPUI  w 5JFSSFRVJSFTW TP"1*TUBCJMJUZBOE RVBMJUZBSFQSJPSJUJ[FE
  7. .$15SBOTQPSUT w .$1IBTUXPDMJFOUTFSWFSUSBOTQPSUT TUEJPBOE4USFBNBCMF)551 w 'PS3VCP$PQBMPOF TUEJPJTTV ffi DJFOU w

    'PSXFCBQQMJDBUJPOT FH 3BJMT3BDL  4USFBNBCMF)551CFDPNFTJNQPSUBOU w $IPPTFCBTFEPOMPDBMQSPDFTTPS)551 💡
  8. 4UEJP#BTJD.FDIBOJTN w $MJFOUTUBSUTTFSWFSWJBOpen3.popen3 JOUFSOBMMZTQBXO  w $MJFOUTFOETSFRVFTUTWJB stdin#puts(json) w 4FSWFSSFBETSFRVFTUTWJB

    stdin#gets w 4FSWFSXSJUFTSFTQPOTFTWJB stdout#puts(json) w $MJFOUSFBETSFTQPOTFTWJB stdout#gets IUUQTNPEFMDPOUFYUQSPUPDPMJPTQFDJ fi DBUJPOCBTJDUSBOTQPSUT MPPQ
  9. require 'mcp' server = MCP::Server.new( name: 'weather', version: '1.0.0', tools:

    [GetForecast] ) transport = MCP::Server::Transports:: StdioTransport.new(server) transport.open class GetForecast < MCP::Tool description "Get forecast for a location" input_schema( properties: { latitude: {type: "number"}, longitude: {type: "number"} }, required: [ "latitude", "longitude" ] ) def self.call(latitude:, longitude:) forecast = fetch_forecast(latitude, longitude) MCP::Tool::Response.new([{ type: "text", text: forecast }]) end 4UEJP&YBNQMF.$14FSWFS IUUQTHJUIVCDPNNPEFMDPOUFYUQSPUPDPMRVJDLTUBSUSFTPVSDFTCMPCNBJOXFBUIFSTFSWFSSVCZXFBUIFSSC weather.rb 'FUDIFTXFBUIFSEBUBGSPN BOFYUFSOBM"1* 5PPMBSHT EF fi OFEBT +40/4DIFNB GPS--. 5IF.$1TFSWFS3VCZ fi MF TQBXOFECZ BO.$1DMJFOU UPDPOOFDUXJUI UIFQBSFOU.$1DMJFOUQSPDFTTWJBTUEJP
  10. class StdioTransport < Transport def open @open = true @session

    = ServerSession.new(server: @server, transport: self) while @open && (line = $stdin.gets) response = @session.handle_json(line.strip) send_response(response) if response end rescue Interrupt warn("\nExiting..."); exit(STATUS_INTERRUPTED) end def send_response(message) json_message = message.is_a?(String) ? message : JSON.generate(message) $stdout.puts(json_message) $stdout.flush end *OTJEF4UEJP5SBOTQPSUPQFO IUUQTHJUIVCDPNNPEFMDPOUFYUQSPUPDPMSVCZTELCMPCWMJCNDQTFSWFSUSBOTQPSUTTUEJP@USBOTQPSUSC -PPQTPWFS$stdin.gets  DBMMThandle_json(json)  BOETFOETUIFSFTQPOTF 8SJUFUIF+40/SFTQPOTFUP TUEPVUGPSUIF.$1DMJFOU
  11. 4USFBNBCMF)551  w *OJUJBMJ[BUJPOIBOETIBLF w  'JSTU TFOEB1045SFRVFTUUP JOJUJBMJ[FBOESFDFJWFUIFSFTQPOTF 

    JODMVEJOHUIFMCP-Session-Id BOETFSWFSDBQBCJMJUJFT w  5IFO TFOECBDL UIFMCP-Session-IdUPDPO fi SN UIFTFTTJPO *OJUJBMJ[BUJPO IUUQTNPEFMDPOUFYUQSPUPDPMJPTQFDJ fi DBUJPOCBTJDUSBOTQPSUT  
  12. 4USFBNBCMF)551  $MJFOU3FRVFTUT w 4FOEB1045SFRVFTUXJUI MCP-Session-Id FH UPPMTMJTU  w

    5IFTFSWFSSFTQPOETXJUI BO44&TUSFBNContent-Type: text/event-stream w 0SBTJOHMF+40/SFTQPOTF Content-Type: application/ json IUUQTNPEFMDPOUFYUQSPUPDPMJPTQFDJ fi DBUJPOCBTJDUSBOTQPSUT
  13. server = MCP::Server.new( name: 'my_server', title: 'Example Server Display Name',

    version: '1.0.0', instructions: 'Use the tools of this server...', tools: [SomeTool, AnotherTool] ) transport = MCP::Server::Transports:: StreamableHTTPTransport.new(server) Rails.application.routes.draw do mount transport => '/mcp' end config/routes.rb 4USFBNBCMF)551&YBNQMF.$14FSWFS 4USFBNBCMF)5515SBOTQPSUBT B3BDLBQQMJDBUJPO 4UBUFGVM FH 3BDL Mechanism of MCP Ruby SDK 0.13+
  14. class StreamableHTTPTransport < Transport # Rack app interface. This transport

    can be mounted as a Rack app. def call(env) handle_request(Rack::Request.new(env)) end 3BJMT*OUFHSBUJPO  *OUFHSBUF4USFBNBCMF)5515SBOTQPSUJOUPB3BDLBQQMJDBUJPOTVDIBT3BJMT Mechanism of MCP Ruby SDK 0.13+
  15. class StreamableHTTPTransport < Transport def handle_request(request) case request.env["REQUEST_METHOD"] when "POST"

    handle_post(request) when "GET" handle_get(request) when "DELETE" handle_delete(request) else method_not_allowed_response end end def handle_post(request) accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES) return accept_error if accept_error body_string = request.body.read session_id = extract_session_id(request) *OTJEF4USFBNBCMF)5515SBOTQPSUIBOEMF@SFRVFTU 3PVUFTSFRVFTUTCZ)551NFUIPE 1045(&5%&-&5& &YUSBDUMCP-Session-IdBOE QSPDFTTFTUIFSFRVFTUBDDPSEJOHMZ def extract_session_id(request) request.env["HTTP_MCP_SESSION_ID"] end
  16. 4FTTJPO.BOBHFNFOU w 4UEJPJTDMJFOUTFSWFS TPOPTFTTJPO NBOBHFNFOUOFFEFE 4UEJPJOUFSOBMMZDSFBUFTBTFTTJPOGPSDPOTJTUFODZ  w 4USFBNBCMF)551JTODMJFOUTFSWFS 

    TPTFTTJPONBOBHFNFOUJTSFRVJSFE w 5IF4%,JNQMFNFOUTTFTTJPONBOBHFNFOU VTJOHMCP-Session-IdEF fi OFEJO UIF.$1TQFD
  17. 4FTTJPO.BOBHFNFOUJO4USFBNBCMF)551 Client A ──┐ ┌── 1 Server Client B ──┤

    Streamable HTTP │ ├── tools, prompts, resources Client C ──┘ │ └── handlers, capabilities │ └── 1 StreamableHTTPTransport @sessions = { A => { stream: io_a, session_a } B => { stream: io_b, session_b } C => { stream: io_c, session_c } } ┌──────────────┬──────────────┐ ▼ ▼ ▼ session_a session_b session_c @client=A @client=B @client=C @logging=error @logging=debug @logging=info @writer=io_a @writer=io_b @writer=io_c notify_*→io_a notify_*→io_b notify_*→io_c N sessions × M concurrent requests on Puma "TFTTJPOJTDSFBUFE GPSFBDIDMJFOU )PMETPOMZTIBSFEDPO fi H MCP-Session-IdJTUIFLFZPG 5SBOTQPSUTJOTUBODFWBSJBCMF DPOOFDUTDMJFOUBOETFSWFSWJB MCP-Session-IdFTUBCMJTIFE EVSJOHUIFJOJUJBMJ[FIBOETIBLF &BDITFTTJPO IPMETQFSDMJFOU DPO fi HVSBUJPO
  18. 4FSWFSUP$MJFOU.FTTBHFT w 4USFBNBCMF)551TVQQPSUTCJEJSFDUJPOBM DPNNVOJDBUJPO )5511045 44&  w 4FSWFSVTFT44&UPTFOENFTTBHFTUP UIFDMJFOU

    w 5XPUZQFT fi SFBOEGPSHFU -PHHJOH  /PUJ fi DBUJPO BOESFTQPOTFBXBJUJOH 3PPUT 4BNQMJOH &MJDJUBUJPO  w 3FTQPOTFBXBJUJOHPOFTSFDFJWF SFTQPOTFTWJBBTFQBSBUFSFRVFTU ˡ44&NFTTBHFTGSPNTFSWFS IUUQTNPEFMDPOUFYUQSPUPDPMJPTQFDJ fi DBUJPOCBTJDUSBOTQPSUT
  19. 44& 4FSWFS4FOU&WFOUT w "OPSNBM)551SFTQPOTF application/json  SFUVSOTPOF+40/BOEDMPTFT /PU44&  w

    44& text/event-stream LFFQTUIFDPOOFDUJPO PQFOBOETFOETNVMUJQMFNFTTBHFTBTdata:MJOFT JOBTJOHMF)551SFTQPOTF HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache data: {"jsonrpc":"2.0","method":"notifications/progress","params":{...}} data: {"jsonrpc":"2.0","method":"notifications/progress","params":{...}} data: {"jsonrpc":"2.0","id":"1","result":{"tools":[...]}} )5513FTQPOTF
  20. 44& 4FSWFS4FOU&WFOUT def DataProcessingTool.call(server_context:) server_context.report_progress(0, total: 100) data = fetch_data

    server_context.report_progress(50, total: 100) result = process(data) server_context.report_progress(100, total: 100) # data: {"method":"notifications/progress",...}\n\n MCP::Tool::Response.new([{type: "text", text: result}]) end def handle_request_with_sse_response(...) body = proc do |stream| @sessions[session_id][:post_request_streams][request_id] = stream response = dispatch_handle_json(body_string, server_session) send_to_stream(stream, response) # data: {"jsonrpc":"2.0","id":"1","result":{...}}\n\n stream.close end [200, SSE_HEADERS, body] end def send_to_stream(stream, data) stream.write( "data: #{data.to_json}\n\n" ) stream.flush end 5SBOTQPSU 4%,JOUFSOBM 5PPM VTFSDPEF DBMMFEJOTJEFEJTQBUDI@IBOEMF@KTPO 3BDL4USFBNJOH#PEZ 1VNBDBMMTCPEZDBMM TUSFBN   # data: {"jsonrpc":"2.0","id":"1","result":{...}}\n\n  # data: {"jsonrpc":"2.0","id":"1","result":{...}}\n\n  # data: {"jsonrpc":"2.0","id":"1","result":{...}}\n\n  # data: {"jsonrpc":"2.0","id":"1","result":{...}}\n\n
  21. $SPTTSFRVFTUTZODWJB2VFVF @pending_responses (shared hash on StreamableHTTPTransport) ┌──────────────────────────────────┐ │ request_id_A =>

    { queue: QueueA }│←protected by @mutex (prevents TOCTOU) │ request_id_B => { queue: QueueB }│ └──────────────────────────────────┘ ↑ register ↑ lookup │ │ Request 1 Request 2 (tools/call) (sampling result POST with │ with request_id_A) Generate request_id_A │ Create QueueA │ Store in @pending │ │ │ ←── Send request_id_A │ to client via SSE │ │ Lookup QueueA by request_id_A │ │ QueueA.pop ←──────── QueueA.push(result) (block) (unblock) │ Receives result Resumes tool execution ←── Sends Tool::Response via SSE )BTILFZFECZ SFRVFTU@JE SFRVFTUUPPMTDBMM  (FOFSBUF SFRVFTU@JEBOE 2VFVF"  3FTQPOTFUPDMJFOU GSPNSFRVFTU  #MPDLIFSF  SFRVFTU3FTQPOTF GSPNDMJFOUXJUI SFRVFTU@JE  -PPLVQ2VFVF" GSPN !QFOEJOH@SFTQPOTFT  1VTIUP2VFVF"  QPQSFRSFTVNFT  💡When a response from the client is needed inside tools/call 3FTQPOETUPDMJFOU BOEDPNQMFUFT 
  22. # Request 2 # client response POST def handle_response(body, ...)

    pending = @pending_responses[body[:id]] pending[:queue].push(body[:result]) end # Request 1 # tools/call def send_request(method, ...) request_id = generate_request_id # SecureRandom.uuid queue = Queue.new @pending_responses[request_id] = {queue:} request = {jsonrpc: "2.0", id: request_id, method:} send_to_stream(stream, request) response = queue.pop # returns to caller, resumes tool execution. response end $SPTTSFRVFTUTZODWJB2VFVF IUUQTEPDTSVCZMBOHPSHFO5ISFBE2VFVFIUNM 2VFVF JOJWBS  $SFBUFBRVFVF  8BJUUIFRVFVF  &ORVFVFUIF DMJFOUSFTQPOTF %FRVFVF UIFSFTQPOTF 
  23. class McpController < ActionController::API def create # POST server =

    MCP::Server.new( name: 'weather', version: '1.0.0', tools: [GetAlert, GetForecast] ) transport = MCP::Server::Transports:: StreamableHTTPTransport.new(server, stateless: true) server.transport = transport status, headers, body = transport.handle_request(request) render(json: body.first, status:, headers:) end 4USFBNBCMF)551&YBNQMF.$14FSWFS 💡Stateless Mode: available on Puma (workers >= 2) or Unicorn 4UBUFMFTT FH "DUJPO.FUIPET TUBUFMFTTUSVFCFDBVTFB OFXUSBOTQPSUJOTUBODFJT DSFBUFEQFSSFRVFTU TP MCP-Session-IdDBOOPU CFTIBSFE .$1QSPUPDPMSFRVFTUIBOEMJOH
  24. 3VCP$PQT#VJMUJO.$1 w "OFYQFSJNFOUBMGFBUVSFJOUSPEVDFEJO 3VCP$PQ w 3FRVJSFTgem 'mcp'JOUIF(FN fi MF w

    "WBJMBCMFCZSFHJTUFSJOHUIF.$1TFSWFS VTJOHNDQKTPO FH JO$MBVEF$PEF
  25. w "VUPDPSSFDU--.HFOFSBUFE3VCZDPEF OFFETMJOUJOHGPSNBUUJOH w *OTQFDUJPO-41+40/31$TUZMF OPU DMBOHTUZMFEJBHOPTUJDT XJUIP ff FOTFTVNNBSZ

    'FBUVSFTJO3VCP$PQ ❯ /mcp ─────────────────────────────────────────────────────────────────────────────── Tools for rubocop 2 tools ❯ 1. RuboCop's inspection read-only 2. RuboCop's autocorrection destructive $MBVEF$PEF
  26. w *T$MBOHGPSNBU QSPHSBNNBUJDBMMZ QSPDFTTBCMF w +40/31$JO-41BT BXFMMLOPXOGPSNBU w +40/JTFBTJFSGPS BHFOUTUPQSPDFTT

    QSPHSBNNBUJDBMMZ -41GPSNBU Offenses: test.rb:10:20: C: Style/RedundantLineContinuation: Redundant line continuation. "longlong-string \ ... ^ 1 file inspected, 1 offense detected { "method": "textDocument/publishDiagnostics", "params": { "uri": "file:///path/to/test.rb", "diagnostics": [{ "message": "Redundant line continuation.", "code": "Style/RedundantLineContinuation", "range": { "start": { "line": 9, "character": 19 }, "end": { "line": 9, "character": 20 } } }] } } $MBOH'PSNBU -41 +40/31$
  27. Built-in LSP 3VCP$PQ 3VOOFS SVO DBMM SFRVFTU EFMFHBUF 3VCP$PQ$-* $PNNBOE

    -41 rubocop --lsp 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%& 3VCP$PQ-41 3PVUFT 3VCP$PQ-41 3VOUJNF P ff FOTFT GPSNBU XSJUF TUBSU IUUQTSVCZLBJHJPSHQSFTFOUBUJPOTLPJDIUNMEBZ 3VCP$PQTCVJMUJO-41 Request / response (JSON-RPC) 💡This design was first introduced at RubyKaigi 2023
  28. Built-in LSP Request / response (JSON-RPC) 3VCP$PQ 3VOOFS SVO DBMM

    SFRVFTU EFMFHBUF 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%& 3VCP$PQ-41 3PVUFT 3VCP$PQ-41 3VOUJNF P ff FOTFT GPSNBU XSJUF TUBSU SVCPDPQNDQ "HFOU Built-in MCP 3VC$PQ$-* $PNNBOE .$1 3VCP$PQ.$1 4FSWFS 3VCP$PQTCVJMUJO.$1 --. .$1$MJFOU Request / Response (JSON-RPC) "HFOU TUBSU 💡Based on RuboCop's built-in LSP 6TFTTUEJPUSBOTQPSU BOEUPPMTJOUFSOBMMZ
  29. *OTJEF3VCP$PQ.$14FSWFS class RuboCop::MCP::Server def initialize(config_store) = (@runtime = RuboCop::LSP::Runtime.new(config_store)) def

    start server = ::MCP::Server.new( name: 'rubocop_mcp_server', version: RuboCop::Version::STRING, tools: [inspection_tool] ) ::MCP::Server::Transports::StdioTransport.new(server).open end private def inspection_tool ::MCP::Tool.define( name: 'rubocop_inspection', description: 'Inspect Ruby code for offenses.', input_schema: { properties: {path: {type: 'string'}, source_code: {type: 'string'}} } ) do |path: nil, source_code: nil| offenses = @runtime.offenses(path || 'example.rb', source_code) ::MCP::Tool::Response.new([{type: 'text', text: offenses.to_json}]) end end end
  30. 1SPCBCJMJTUJDWT%FUFSNJOJTUJD w .$1BOE"HFOU4LJMMTBSFOPU HVBSBOUFFEUPFYFDVUF w "HFOU4LJMMTTQFDTBZTdescription EF fi OFTXIFOUPVTF w

    3VCP$PQBVUPDPSSFDUPOBHFOUDPEF TIPVMEVTF1PTU5PPM6TFIPPLT OPU.$1 IUUQTDPEFDMBVEFDPNEPDTFOIPPLTQPTUUPPMVTF
  31. { "hooks": { "PostToolUse": [{ "matcher": "Edit|Write", "hooks": [{ "type":

    "command", "command": "file_path=$(cat - | jq -r '.tool_input.file_path // empty') && [ -n \"$file_path\" ] && bundle exec rubocop -a --force-exclusion \"$file_path\" 2>/dev/null || true" }] }] } } 1PTU5PPM6TFXJUI3VCP$PQ 3VOBVUPDPSSFDUPOUIFHFOFSBUFE fi MFTVTJOHrubocop -a .claude/settings.json ⚠Hooks always execute, so there is a trade-off with token consumption
  32. -41WT.$1$POTUSBJOFEWT0QFO&OEFE w -41EF fi OFTB fi YFETFUPGDBQBCJMJUJFT  TPXIBUUPCVJMEJTDPOTUSBJOFECZUIF QSPUPDPM

    w .$1JTPQFOFOEFEXJUIQSPCBCJMJTUJD OBUVSBMMBOHVBHF TPXIBUUPCVJME EFQFOETPOJNBHJOBUJPO
  33. %PZPVIBWF"OZ*EFBT 🤔 w FH JOTQFDUXIJDIDPQTBSFBWBJMBCMF FOBCMF EJTBCMFEDPQTUFNQPSBSJMZ BOEDIFDLDPEF JODMVEJOHEJTBCMFEDPQT w

    #VJMEUIFCFTUDPO fi HGPSBQSPKFDUTDPEF w 0UIFSWBHVFJEFBTTPGBS MFWFSBHJOHQSPCBCJMJTUJDFNFSHFODFUISPVHI )VNBOJOUIF-PPQ🤔
  34. 4FSWFSUP$MJFOU3FRVFTUT w 'PS--.BOEGPSIVNBO w 4BNQMJOH GPS--.  w &MJDJUBUJPO GPSIVNBO

     w "WBJMBCMF)VNBOJOUIF -PPQ ˡ44&NFTTBHFTGSPNTFSWFS IUUQTNPEFMDPOUFYUQSPUPDPMJPTQFDJ fi DBUJPOCBTJDUSBOTQPSUT
  35. class SummarizeTool < MCP::Tool description "Summarize text using LLM" input_schema(

    properties: {text: {type: "string"}}, required: ["text"] ) def self.call(text:, server_context:) result = server_context.create_sampling_message( messages: [ {role: "user", content: {type: "text", text: "Please summarize: #{text}"}} ], max_tokens: 500 ) MCP::Tool::Response.new([{ type: "text", text: result[:content][:text] }]) end end 4BNQMJOH&YBNQMF.$14FSWFS create_sampling_message TZODISPOPVTMZTFOETBO--. SFRVFTUUPUIFDMJFOUWJB44&BOE CMPDLTVOUJMUIFSFTQPOTFJTSFUVSOFE 3FUVSOTUIFSFTQPOTFUPUIFDMJFOU  JODPSQPSBUJOHUIF4BNQMJOHSFTVMU
  36. &MJDJUBUJPO )VNBO w .$1TFSWFSQBVTFTUBTLFYFDVUJPOUP SFRVFTUVTFSJOQVUWJBGPSNPS63- BQQSPWBM FH 0"VUI  w

    &OBCMFT)VNBOJOUIF-PPQ w $MBVEF$PEFTVQQPSUTUIJTTJODFW QFS$)"/(&-0(NEPGBOUISPQJDTDMBVEFDPEF
  37. class ConfirmDeleteTool < MCP::Tool description "Delete a record with user

    confirmation" input_schema(properties: {id: {type: "string"}}, required: ["id"]) def self.call(id:, server_context:) result = server_context.create_form_elicitation( message: "Are you sure you want to delete record #{id}?", requested_schema: { type: "object", properties: {confirm: {type: "boolean", description: "Confirm deletion"}}, required: ["confirm"] } ) if result[:content][:confirm] delete_record(id) text = "Record #{id} deleted." else text = "Deletion cancelled." end MCP::Tool::Response.new([{type: "text", text: text}]) end end &MJDJUBUJPO&YBNQMF.$14FSWFS 3FUVSOTUIFSFTQPOTFUPUIFDMJFOU  JODPSQPSBUJOHUIF&MJDJUBUJPOSFTVMU create_elicitation TFOETBGPSNSFRVFTU UPUIFDMJFOUWJB44& BOECMPDLTVOUJMUIF VTFSTVCNJUTUIFJOQVU requested_schemaEF fi OFTUIFGPSN fi FMET UIFVTFSNVTU fi MMJO +40/4DIFNB