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:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"eminuxcrm": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:3001/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${CRM_API_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Codex legge ~/.codex/config.toml (o CODEX_HOME).
|
||||
# Crea un token in CRM → Token API, poi:
|
||||
|
||||
[mcp_servers.eminuxcrm]
|
||||
url = "http://localhost:3001/mcp"
|
||||
bearer_token_env_var = "CRM_API_TOKEN"
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"eminuxcrm": {
|
||||
"url": "http://localhost:3001/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${env:CRM_API_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
require "net/http"
|
||||
require "uri"
|
||||
require "mcp"
|
||||
|
||||
module EminuxCrmMcp
|
||||
module Client
|
||||
module_function
|
||||
|
||||
def crm_url
|
||||
ENV.fetch("CRM_URL", "http://localhost:3001").to_s.sub(%r{/+\z}, "")
|
||||
end
|
||||
|
||||
def api_token
|
||||
ENV.fetch("CRM_API_TOKEN", "")
|
||||
end
|
||||
|
||||
def request(method, path, body: nil)
|
||||
if api_token.strip.empty?
|
||||
return error_payload("Imposta CRM_API_TOKEN (token creato in CRM → Token API).")
|
||||
end
|
||||
|
||||
uri = URI.parse("#{crm_url}#{path}")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = uri.scheme == "https"
|
||||
http.open_timeout = 10
|
||||
http.read_timeout = 30
|
||||
|
||||
request = http_class(method).new(uri)
|
||||
request["Authorization"] = "Bearer #{api_token}"
|
||||
request["Accept"] = "application/json"
|
||||
if body
|
||||
request["Content-Type"] = "application/json"
|
||||
request.body = JSON.generate(body)
|
||||
end
|
||||
|
||||
response = http.request(request)
|
||||
payload = parse_json(response.body)
|
||||
text = JSON.pretty_generate(payload)
|
||||
MCP::Tool::Response.new([ { type: "text", text: text } ], error: response.code.to_i >= 400)
|
||||
rescue StandardError => e
|
||||
error_payload("#{e.class}: #{e.message}")
|
||||
end
|
||||
|
||||
def http_class(method)
|
||||
{
|
||||
get: Net::HTTP::Get,
|
||||
post: Net::HTTP::Post,
|
||||
patch: Net::HTTP::Patch
|
||||
}.fetch(method)
|
||||
end
|
||||
|
||||
def parse_json(raw)
|
||||
JSON.parse(raw.to_s)
|
||||
rescue JSON::ParserError
|
||||
{ error: "Risposta non JSON", body: raw.to_s[0, 2000] }
|
||||
end
|
||||
|
||||
def error_payload(message)
|
||||
MCP::Tool::Response.new([ { type: "text", text: message } ], error: true)
|
||||
end
|
||||
|
||||
def compact(hash)
|
||||
hash.each_with_object({}) do |(key, value), acc|
|
||||
acc[key] = value unless value.nil? || value == ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ListProjects < MCP::Tool
|
||||
description "Elenca i progetti CRM accessibili all'utente del token."
|
||||
|
||||
class << self
|
||||
def call(server_context: nil)
|
||||
Client.request(:get, "/api/v1/projects")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Today < MCP::Tool
|
||||
description "Cosa fare oggi nel progetto: 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)
|
||||
Client.request(:get, "/api/v1/p/#{encode(project_code)}/today")
|
||||
end
|
||||
|
||||
def encode(value)
|
||||
URI.encode_www_form_component(value.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Search < MCP::Tool
|
||||
description "Cerca organizzazioni, contatti e opportunità nel progetto."
|
||||
input_schema(
|
||||
properties: {
|
||||
project_code: { type: "string", description: "Codice progetto, es. matchlivetv" },
|
||||
q: { type: "string", description: "Testo di ricerca" }
|
||||
},
|
||||
required: %w[project_code q]
|
||||
)
|
||||
|
||||
class << self
|
||||
def call(project_code:, q:, server_context: nil)
|
||||
query = URI.encode_www_form(q: q)
|
||||
Client.request(:get, "/api/v1/p/#{Today.encode(project_code)}/search?#{query}")
|
||||
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)
|
||||
Client.request(:get, "/api/v1/p/#{Today.encode(project_code)}/organizations/#{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)
|
||||
Client.request(
|
||||
:post,
|
||||
"/api/v1/p/#{Today.encode(project_code)}/tasks",
|
||||
body: Client.compact(
|
||||
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
|
||||
)
|
||||
)
|
||||
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)
|
||||
Client.request(:post, "/api/v1/p/#{Today.encode(project_code)}/tasks/#{id}/complete")
|
||||
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)
|
||||
Client.request(
|
||||
:post,
|
||||
"/api/v1/p/#{Today.encode(project_code)}/activities",
|
||||
body: Client.compact(
|
||||
organization_id: organization_id,
|
||||
activity_type: activity_type,
|
||||
subject: subject,
|
||||
description: description,
|
||||
happened_at: happened_at,
|
||||
contact_id: contact_id,
|
||||
opportunity_id: opportunity_id
|
||||
)
|
||||
)
|
||||
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)
|
||||
Client.request(
|
||||
:patch,
|
||||
"/api/v1/p/#{Today.encode(project_code)}/opportunities/#{id}/stage",
|
||||
body: Client.compact(
|
||||
pipeline_stage: pipeline_stage,
|
||||
lost_reason: lost_reason,
|
||||
notes: notes
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
server = MCP::Server.new(
|
||||
name: "eminuxcrm",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
EminuxCrmMcp::ListProjects,
|
||||
EminuxCrmMcp::Today,
|
||||
EminuxCrmMcp::Search,
|
||||
EminuxCrmMcp::GetOrganization,
|
||||
EminuxCrmMcp::CreateTask,
|
||||
EminuxCrmMcp::CompleteTask,
|
||||
EminuxCrmMcp::CreateActivity,
|
||||
EminuxCrmMcp::UpdateOpportunityStage
|
||||
]
|
||||
)
|
||||
|
||||
transport = MCP::Server::Transports::StdioTransport.new(server)
|
||||
transport.open if $PROGRAM_NAME == __FILE__ || File.basename($PROGRAM_NAME.to_s) == "crm-mcp"
|
||||
Reference in New Issue
Block a user