# frozen_string_literal: true require "json" require "mcp" module EminuxCrmMcp INSTRUCTIONS = <<~TEXT.freeze Sei collegato a eminuxCRM. Quando l'utente chiede di lavorare su prospect, pipeline, task o follow-up, usa questi tool (non curl). 1. list_projects per i project_code (matchlivetv, riskmeter, cardoo, …). 2. today per capire cosa fare oggi. 3. search / get_organization per il contesto. 4. create_activity, complete_task, create_task, update_opportunity_stage per agire. Non inviare mailing, non cancellare record, non gestire utenti. TEXT module_function def allowed_hosts hosts = %w[localhost 127.0.0.1 [::1] web] hosts.concat(configured_public_hosts) hosts << "www.example.com" if defined?(Rails) && Rails.env.test? hosts.map { |host| normalize_host(host) }.compact.uniq end def configured_public_hosts values = [ ENV["APP_HOST"] ] values.concat(ENV.fetch("RAILS_ALLOWED_HOSTS", "").split(",")) values end def normalize_host(value) host = value.to_s.strip return if host.blank? host = host.sub(%r{\Ahttps?://}i, "") host.split("/").first end def server_for(user:) MCP::Server.new( name: "eminuxcrm", version: "1.0.0", instructions: INSTRUCTIONS, server_context: { user: user }, tools: [ ListProjects, Today, Search, GetOrganization, CreateTask, CompleteTask, CreateActivity, UpdateOpportunityStage ] ) end def json_response(result) MCP::Tool::Response.new( [ { type: "text", text: JSON.pretty_generate(result.body.as_json) } ], error: !result.ok ) end def session_from(server_context) user = extract_user(server_context) raise "MCP senza utente autenticato" if user.nil? Crm::AgentSession.new(user) end def extract_user(ctx) return if ctx.nil? return ctx[:user] if ctx.is_a?(Hash) return ctx.user if ctx.respond_to?(:user) && ctx.method(:user).arity.zero? inner = ctx.instance_variable_get(:@context) if ctx.respond_to?(:instance_variable_get) return inner[:user] if inner.is_a?(Hash) ctx[:user] if ctx.respond_to?(:[]) end class ListProjects < MCP::Tool description "Elenca i progetti CRM accessibili all'utente del token (codici da usare negli altri tool)." class << self def call(server_context: nil) EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).projects) end end end class Today < MCP::Tool description "Cosa fare oggi: task scaduti, di oggi, in arrivo e opportunità ferme da 7+ giorni." input_schema( properties: { project_code: { type: "string", description: "Codice progetto, es. matchlivetv" } }, required: %w[project_code] ) class << self def call(project_code:, server_context: nil) EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).today(project_code)) end end end class Search < MCP::Tool description "Cerca organizzazioni, contatti e opportunità nel progetto." input_schema( properties: { project_code: { type: "string" }, q: { type: "string", description: "Testo di ricerca" } }, required: %w[project_code q] ) class << self def call(project_code:, q:, server_context: nil) EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).search(project_code, q)) end end end class GetOrganization < MCP::Tool description "Scheda organizzazione: anagrafica, contatti, opportunità aperte, task pending e ultime attività." input_schema( properties: { project_code: { type: "string" }, id: { type: "integer", description: "ID organizzazione" } }, required: %w[project_code id] ) class << self def call(project_code:, id:, server_context: nil) EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).organization(project_code, id)) end end end class CreateTask < MCP::Tool description "Crea un task (follow-up, chiamata, email, …) su un'organizzazione del progetto." input_schema( properties: { project_code: { type: "string" }, organization_id: { type: "integer" }, title: { type: "string" }, due_at: { type: "string", description: "ISO8601, es. 2026-09-03T10:00:00+02:00" }, description: { type: "string" }, contact_id: { type: "integer" }, opportunity_id: { type: "integer" }, assigned_user_id: { type: "integer" }, priority: { type: "string", description: "low, normal, high, urgent" }, task_type: { type: "string", description: "follow_up, call, email, meeting, demo, proposal, generic" } }, required: %w[project_code organization_id title due_at] ) class << self def call(project_code:, organization_id:, title:, due_at:, description: nil, contact_id: nil, opportunity_id: nil, assigned_user_id: nil, priority: nil, task_type: nil, server_context: nil) attrs = { organization_id: organization_id, title: title, due_at: due_at, description: description, contact_id: contact_id, opportunity_id: opportunity_id, assigned_user_id: assigned_user_id, priority: priority, task_type: task_type }.compact EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).create_task(project_code, attrs)) end end end class CompleteTask < MCP::Tool description "Segna un task come completato e registra l'attività in timeline." input_schema( properties: { project_code: { type: "string" }, id: { type: "integer", description: "ID task" } }, required: %w[project_code id] ) class << self def call(project_code:, id:, server_context: nil) EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).complete_task(project_code, id)) end end end class CreateActivity < MCP::Tool description "Annota un'attività in timeline (nota, chiamata, email inviata/ricevuta, demo, proposta, …)." input_schema( properties: { project_code: { type: "string" }, organization_id: { type: "integer" }, activity_type: { type: "string", description: "note, email_sent, email_received, call, meeting, demo, follow_up, proposal_sent, other" }, subject: { type: "string" }, description: { type: "string" }, happened_at: { type: "string", description: "ISO8601; default ora" }, contact_id: { type: "integer" }, opportunity_id: { type: "integer" } }, required: %w[project_code organization_id activity_type subject] ) class << self def call(project_code:, organization_id:, activity_type:, subject:, description: nil, happened_at: nil, contact_id: nil, opportunity_id: nil, server_context: nil) attrs = { organization_id: organization_id, activity_type: activity_type, subject: subject, description: description, happened_at: happened_at, contact_id: contact_id, opportunity_id: opportunity_id }.compact EminuxCrmMcp.json_response(EminuxCrmMcp.session_from(server_context).create_activity(project_code, attrs)) end end end class UpdateOpportunityStage < MCP::Tool description "Sposta un'opportunità di pipeline (to_contact, contacted, replied, interested, demo_trial, first_use, proposal, won, lost)." input_schema( properties: { project_code: { type: "string" }, id: { type: "integer", description: "ID opportunità" }, pipeline_stage: { type: "string" }, lost_reason: { type: "string", description: "Obbligatorio se stage = lost" }, notes: { type: "string" } }, required: %w[project_code id pipeline_stage] ) class << self def call(project_code:, id:, pipeline_stage:, lost_reason: nil, notes: nil, server_context: nil) EminuxCrmMcp.json_response( EminuxCrmMcp.session_from(server_context).update_opportunity_stage( project_code, id, pipeline_stage: pipeline_stage, lost_reason: lost_reason, notes: notes ) ) end end end end