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

Exploring RuboCop with MCP

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