Espone API JSON e MCP HTTP per far lavorare gli agenti sul CRM.

Gli agenti autenticati con token Bearer possono leggere today/pipeline e annotare attività, con host MCP allineati a quelli di produzione.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 22:43:46 +02:00
co-authored by Cursor
parent 1f946f63ba
commit 936930e691
44 changed files with 1693 additions and 4 deletions
+257
View File
@@ -0,0 +1,257 @@
# 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