mcp-movidesk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +161 -0
- package/bin/mcp-movidesk.js +2 -0
- package/dist/index.js +18 -0
- package/dist/movideskClient.js +141 -0
- package/dist/tools.js +163 -0
- package/dist/types.js +1 -0
- package/package.json +30 -0
package/.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MOVIDESK_TOKEN=coloque-seu-token-aqui
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Movidesk MCP Server
|
|
2
|
+
|
|
3
|
+
Servidor MCP em TypeScript/Bun para consultar tickets do Movidesk em modo somente leitura.
|
|
4
|
+
|
|
5
|
+
## Ferramentas
|
|
6
|
+
|
|
7
|
+
- `get_ticket`: retorna dados principais do ticket e resumo das ultimas interacoes.
|
|
8
|
+
- `get_ticket_history`: retorna historico, comentarios, status e interacoes do ticket.
|
|
9
|
+
- `get_ticket_attachments`: retorna anexos/imagens associados ao ticket, com metadados, hash e URL de download sem token quando houver hash.
|
|
10
|
+
|
|
11
|
+
Nenhuma ferramenta cria, altera, exclui ou atualiza tickets.
|
|
12
|
+
|
|
13
|
+
## Requisitos
|
|
14
|
+
|
|
15
|
+
- Bun instalado.
|
|
16
|
+
- Token da API Movidesk.
|
|
17
|
+
|
|
18
|
+
## Instalacao
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone <url-do-repositorio> mcp-movidesk
|
|
22
|
+
cd mcp-movidesk
|
|
23
|
+
bun install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Variaveis De Ambiente
|
|
27
|
+
|
|
28
|
+
Crie um arquivo `.env` ou configure a variavel no ambiente do MCP client:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
MOVIDESK_TOKEN=seu-token-da-api-movidesk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
O token nao deve ser versionado ou escrito no codigo.
|
|
35
|
+
|
|
36
|
+
Um arquivo `.env.example` esta incluido apenas como modelo.
|
|
37
|
+
|
|
38
|
+
## Execucao
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bun run src/index.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Ou pelo script:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bun run start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuracao MCP Client
|
|
51
|
+
|
|
52
|
+
Exemplo generico de configuracao para um cliente MCP via stdio:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"movidesk": {
|
|
58
|
+
"command": "bun",
|
|
59
|
+
"args": ["run", "C:/MCP Servers/Movidesk/src/index.ts"],
|
|
60
|
+
"env": {
|
|
61
|
+
"MOVIDESK_TOKEN": "seu-token-da-api-movidesk"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuracao OpenCode
|
|
69
|
+
|
|
70
|
+
Adicione o servidor no arquivo de configuracao do OpenCode, normalmente em `%USERPROFILE%\.opencode\opencode.json` no Windows.
|
|
71
|
+
|
|
72
|
+
Exemplo usando pacote publicado e `bunx`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"movidesk": {
|
|
78
|
+
"command": "bunx",
|
|
79
|
+
"args": ["mcp-movidesk@latest"],
|
|
80
|
+
"env": {
|
|
81
|
+
"MOVIDESK_TOKEN": "seu-token-da-api-movidesk"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Exemplo usando o script `start` a partir de um repositorio clonado:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"movidesk": {
|
|
94
|
+
"command": "bun",
|
|
95
|
+
"args": ["run", "--cwd", "C:/caminho/para/mcp-movidesk", "start"],
|
|
96
|
+
"env": {
|
|
97
|
+
"MOVIDESK_TOKEN": "seu-token-da-api-movidesk"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Depois de alterar a configuracao, reinicie o OpenCode e teste pedindo uma consulta de ticket numerico.
|
|
105
|
+
|
|
106
|
+
## Publicacao
|
|
107
|
+
|
|
108
|
+
Para publicar como pacote executavel e usar via `bunx`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bun install
|
|
112
|
+
bun run build
|
|
113
|
+
npm publish
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Depois de publicado, o OpenCode pode iniciar o MCP com `bunx mcp-movidesk@latest`.
|
|
117
|
+
|
|
118
|
+
Antes de publicar, ajuste o `name` no `package.json` se quiser usar um pacote escopado, por exemplo `@sua-org/mcp-movidesk`. Nesse caso, a configuracao fica `"args": ["@sua-org/mcp-movidesk@latest"]`.
|
|
119
|
+
|
|
120
|
+
Arquivos recomendados para versionar:
|
|
121
|
+
|
|
122
|
+
- `bin/`
|
|
123
|
+
- `scripts/`
|
|
124
|
+
- `src/`
|
|
125
|
+
- `package.json`
|
|
126
|
+
- `bun.lock`
|
|
127
|
+
- `tsconfig.json`
|
|
128
|
+
- `README.md`
|
|
129
|
+
- `.env.example`
|
|
130
|
+
- `.gitignore`
|
|
131
|
+
|
|
132
|
+
Nao versione `.env`, tokens, logs ou `node_modules/`.
|
|
133
|
+
|
|
134
|
+
## API Movidesk Utilizada
|
|
135
|
+
|
|
136
|
+
- Base URL: `https://api.movidesk.com/public/v1`
|
|
137
|
+
- Ticket por ID: `GET /tickets?token=TOKEN&id=TICKET_ID`
|
|
138
|
+
- Historico/interacoes: `GET /tickets` com `$expand=actions(...)`
|
|
139
|
+
- Anexos do ticket: `actions($expand=attachments)`
|
|
140
|
+
- Download de anexo: `GET /storage/download?token=TOKEN&id=HASH`; a ferramenta retorna apenas a URL sem `token` e o `hash`.
|
|
141
|
+
|
|
142
|
+
Observacoes da documentacao Movidesk:
|
|
143
|
+
|
|
144
|
+
- A rota `/tickets` cobre tickets com `lastUpdate` inferior a 90 dias; tickets antigos podem exigir `/tickets/past`.
|
|
145
|
+
- A API possui limite de 10 requisicoes por minuto.
|
|
146
|
+
- Em caso de bloqueio por falhas, a API pode retornar `429` e header `retry-after`.
|
|
147
|
+
- O servidor serializa chamadas para respeitar aproximadamente 10 requisicoes por minuto.
|
|
148
|
+
|
|
149
|
+
## Validacao
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bun run typecheck
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Seguranca
|
|
156
|
+
|
|
157
|
+
- O servidor usa apenas `GET`.
|
|
158
|
+
- O token e lido exclusivamente de `MOVIDESK_TOKEN`.
|
|
159
|
+
- URLs de anexo retornadas pelas ferramentas nao incluem token; use o hash retornado com credenciais fora do historico MCP quando precisar baixar o arquivo.
|
|
160
|
+
- Logs sao minimos e nao exibem token.
|
|
161
|
+
- Respostas grandes sao truncadas/resumidas antes de retornar ao agente.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { MovideskClient } from "./movideskClient.js";
|
|
4
|
+
import { registerTools } from "./tools.js";
|
|
5
|
+
const token = process.env.MOVIDESK_TOKEN;
|
|
6
|
+
if (!token) {
|
|
7
|
+
console.error("MOVIDESK_TOKEN nao configurado.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: "mcp-movidesk",
|
|
12
|
+
version: "0.1.0"
|
|
13
|
+
});
|
|
14
|
+
const client = new MovideskClient({ token });
|
|
15
|
+
registerTools(server, client);
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
console.error("Movidesk MCP server iniciado via stdio.");
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://api.movidesk.com/public/v1";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
3
|
+
const MIN_REQUEST_INTERVAL_MS = 6_100;
|
|
4
|
+
let nextRequestAt = 0;
|
|
5
|
+
export class MovideskApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
retryAfterSeconds;
|
|
8
|
+
constructor(details) {
|
|
9
|
+
super(details.message);
|
|
10
|
+
this.name = "MovideskApiError";
|
|
11
|
+
this.status = details.status;
|
|
12
|
+
this.retryAfterSeconds = details.retryAfterSeconds;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class MovideskClient {
|
|
16
|
+
baseUrl;
|
|
17
|
+
token;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.token = options.token;
|
|
21
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
22
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
23
|
+
}
|
|
24
|
+
async getTicket(ticketId) {
|
|
25
|
+
return this.getTicketWithFallback(ticketId, ticketExpand());
|
|
26
|
+
}
|
|
27
|
+
async getTicketHistory(ticketId) {
|
|
28
|
+
return this.getTicketWithFallback(ticketId, historyExpand());
|
|
29
|
+
}
|
|
30
|
+
async getTicketAttachments(ticketId) {
|
|
31
|
+
return this.getTicketWithFallback(ticketId, attachmentsExpand());
|
|
32
|
+
}
|
|
33
|
+
getAttachmentDownloadUrl(hash) {
|
|
34
|
+
const url = new URL(`${this.baseUrl}/storage/download`);
|
|
35
|
+
url.searchParams.set("id", hash);
|
|
36
|
+
return url.toString();
|
|
37
|
+
}
|
|
38
|
+
async getTicketWithFallback(ticketId, expand) {
|
|
39
|
+
try {
|
|
40
|
+
return await this.getTicketByRoute("tickets", ticketId, expand);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof MovideskApiError && error.status === 404) {
|
|
44
|
+
return this.getTicketByRoute("tickets/past", ticketId, expand);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async getTicketByRoute(route, ticketId, expand) {
|
|
50
|
+
const result = await this.get(`/${route}`, {
|
|
51
|
+
id: ticketId,
|
|
52
|
+
"$expand": expand
|
|
53
|
+
});
|
|
54
|
+
if (!result || typeof result !== "object") {
|
|
55
|
+
throw new MovideskApiError({ message: `Ticket ${ticketId} nao encontrado.` });
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
async get(path, params) {
|
|
60
|
+
await waitForRateLimit();
|
|
61
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
62
|
+
url.searchParams.set("token", this.token);
|
|
63
|
+
for (const [key, value] of Object.entries(params)) {
|
|
64
|
+
url.searchParams.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers: { Accept: "application/json" },
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw await toApiError(response);
|
|
76
|
+
}
|
|
77
|
+
return (await response.json());
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof MovideskApiError) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
84
|
+
throw new MovideskApiError({ message: `Timeout ao consultar a API Movidesk apos ${this.timeoutMs}ms.` });
|
|
85
|
+
}
|
|
86
|
+
throw new MovideskApiError({ message: error instanceof Error ? error.message : "Erro desconhecido ao consultar a API Movidesk." });
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function ticketExpand() {
|
|
94
|
+
return [
|
|
95
|
+
"owner",
|
|
96
|
+
"createdBy",
|
|
97
|
+
"clients",
|
|
98
|
+
"actions($select=id,type,origin,status,justification,createdDate,createdBy,description,htmlDescription,isDeleted)"
|
|
99
|
+
].join(",");
|
|
100
|
+
}
|
|
101
|
+
function historyExpand() {
|
|
102
|
+
return [
|
|
103
|
+
"actions($select=id,type,origin,status,justification,createdDate,createdBy,description,htmlDescription,isDeleted,tags)",
|
|
104
|
+
"actions($expand=attachments)"
|
|
105
|
+
].join(",");
|
|
106
|
+
}
|
|
107
|
+
function attachmentsExpand() {
|
|
108
|
+
return [
|
|
109
|
+
"actions($select=id,createdDate,createdBy)",
|
|
110
|
+
"actions($expand=attachments)"
|
|
111
|
+
].join(",");
|
|
112
|
+
}
|
|
113
|
+
async function toApiError(response) {
|
|
114
|
+
const retryAfter = response.headers.get("retry-after");
|
|
115
|
+
await safeDrainBody(response);
|
|
116
|
+
const message = retryAfter
|
|
117
|
+
? `Movidesk retornou HTTP ${response.status}. Tente novamente apos ${retryAfter}s.`
|
|
118
|
+
: `Movidesk retornou HTTP ${response.status}.`;
|
|
119
|
+
return new MovideskApiError({
|
|
120
|
+
status: response.status,
|
|
121
|
+
message,
|
|
122
|
+
retryAfterSeconds: retryAfter
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function safeDrainBody(response) {
|
|
126
|
+
try {
|
|
127
|
+
await response.text();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore body read failures while preserving the HTTP status error.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function waitForRateLimit() {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const scheduledAt = Math.max(now, nextRequestAt);
|
|
136
|
+
nextRequestAt = scheduledAt + MIN_REQUEST_INTERVAL_MS;
|
|
137
|
+
const delayMs = scheduledAt - now;
|
|
138
|
+
if (delayMs > 0) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MovideskApiError } from "./movideskClient.js";
|
|
3
|
+
const TicketInputShape = {
|
|
4
|
+
ticketId: z.string().trim().regex(/^\d{1,30}$/, "ticketId deve conter apenas numeros")
|
|
5
|
+
};
|
|
6
|
+
const MAX_TEXT_LENGTH = 2_000;
|
|
7
|
+
const MAX_ACTIONS = 40;
|
|
8
|
+
export function registerTools(server, client) {
|
|
9
|
+
const registrar = server;
|
|
10
|
+
registrar.tool("get_ticket", "Consulta dados principais de um ticket Movidesk por ID. Ferramenta somente leitura.", TicketInputShape, async (input) => asToolResult(async () => summarizeTicket(await client.getTicket(input.ticketId))));
|
|
11
|
+
registrar.tool("get_ticket_history", "Consulta historico, comentarios, status e interacoes de um ticket Movidesk. Ferramenta somente leitura.", TicketInputShape, async (input) => asToolResult(async () => summarizeHistory(await client.getTicketHistory(input.ticketId))));
|
|
12
|
+
registrar.tool("get_ticket_attachments", "Lista anexos e imagens associados as acoes de um ticket Movidesk, incluindo metadados e URL de download sem token quando ha hash. Ferramenta somente leitura.", TicketInputShape, async (input) => asToolResult(async () => summarizeAttachments(await client.getTicketAttachments(input.ticketId), client)));
|
|
13
|
+
}
|
|
14
|
+
async function asToolResult(read) {
|
|
15
|
+
try {
|
|
16
|
+
const data = await read();
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, data }, null, 2) }]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const data = formatError(error);
|
|
23
|
+
return {
|
|
24
|
+
isError: true,
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: data }, null, 2) }]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function summarizeTicket(ticket) {
|
|
30
|
+
const actions = Array.isArray(ticket.actions) ? ticket.actions : [];
|
|
31
|
+
return cleanObject({
|
|
32
|
+
id: ticket.id,
|
|
33
|
+
protocol: ticket.protocol,
|
|
34
|
+
subject: truncate(ticket.subject),
|
|
35
|
+
type: ticket.type,
|
|
36
|
+
status: ticket.status,
|
|
37
|
+
baseStatus: ticket.baseStatus,
|
|
38
|
+
justification: ticket.justification,
|
|
39
|
+
category: ticket.category,
|
|
40
|
+
urgency: ticket.urgency,
|
|
41
|
+
origin: ticket.origin,
|
|
42
|
+
createdDate: ticket.createdDate,
|
|
43
|
+
lastUpdate: ticket.lastUpdate,
|
|
44
|
+
lastActionDate: ticket.lastActionDate,
|
|
45
|
+
actionCount: ticket.actionCount,
|
|
46
|
+
ownerTeam: ticket.ownerTeam,
|
|
47
|
+
owner: summarizePerson(ticket.owner),
|
|
48
|
+
createdBy: summarizePerson(ticket.createdBy),
|
|
49
|
+
clients: ticket.clients?.map(summarizePerson),
|
|
50
|
+
serviceFull: ticket.serviceFull,
|
|
51
|
+
tags: ticket.tags,
|
|
52
|
+
dates: cleanObject({ resolvedIn: ticket.resolvedIn, reopenedIn: ticket.reopenedIn, closedIn: ticket.closedIn }),
|
|
53
|
+
recentActions: actions.slice(-5).map(summarizeAction),
|
|
54
|
+
note: actions.length > 5 ? `Retornadas as 5 acoes mais recentes de ${actions.length}. Use get_ticket_history para mais contexto.` : undefined
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function summarizeHistory(ticket) {
|
|
58
|
+
const actions = Array.isArray(ticket.actions) ? ticket.actions : [];
|
|
59
|
+
const visibleActions = actions.slice(-MAX_ACTIONS);
|
|
60
|
+
return cleanObject({
|
|
61
|
+
ticketId: ticket.id,
|
|
62
|
+
subject: truncate(ticket.subject),
|
|
63
|
+
status: ticket.status,
|
|
64
|
+
totalActions: actions.length,
|
|
65
|
+
returnedActions: visibleActions.length,
|
|
66
|
+
truncated: actions.length > visibleActions.length,
|
|
67
|
+
actions: visibleActions.map(summarizeAction)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function summarizeAttachments(ticket, client) {
|
|
71
|
+
const actions = Array.isArray(ticket.actions) ? ticket.actions : [];
|
|
72
|
+
const attachments = actions.flatMap((action) => (action.attachments ?? []).map((attachment) => summarizeAttachment(attachment, action, client)));
|
|
73
|
+
return cleanObject({
|
|
74
|
+
ticketId: ticket.id,
|
|
75
|
+
subject: truncate(ticket.subject),
|
|
76
|
+
totalAttachments: attachments.length,
|
|
77
|
+
attachments
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function summarizeAction(action) {
|
|
81
|
+
return cleanObject({
|
|
82
|
+
id: action.id,
|
|
83
|
+
type: action.type,
|
|
84
|
+
origin: action.origin,
|
|
85
|
+
status: action.status,
|
|
86
|
+
justification: action.justification,
|
|
87
|
+
createdDate: action.createdDate,
|
|
88
|
+
createdBy: summarizePerson(action.createdBy),
|
|
89
|
+
isDeleted: action.isDeleted,
|
|
90
|
+
description: truncate(stripHtml(action.description ?? action.htmlDescription ?? "")),
|
|
91
|
+
attachmentCount: action.attachments?.length ?? 0,
|
|
92
|
+
attachments: action.attachments?.map((attachment) => ({
|
|
93
|
+
fileName: attachment.fileName,
|
|
94
|
+
path: attachment.path,
|
|
95
|
+
createdDate: attachment.createdDate
|
|
96
|
+
})),
|
|
97
|
+
tags: action.tags
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function summarizeAttachment(attachment, action, client) {
|
|
101
|
+
const hash = attachment.path?.trim();
|
|
102
|
+
return cleanObject({
|
|
103
|
+
fileName: attachment.fileName,
|
|
104
|
+
hash,
|
|
105
|
+
downloadUrl: hash ? client.getAttachmentDownloadUrl(hash) : undefined,
|
|
106
|
+
downloadRequiresToken: hash ? true : undefined,
|
|
107
|
+
createdDate: attachment.createdDate,
|
|
108
|
+
createdBy: summarizePerson(attachment.createdBy),
|
|
109
|
+
actionId: action.id,
|
|
110
|
+
actionCreatedDate: action.createdDate,
|
|
111
|
+
actionCreatedBy: summarizePerson(action.createdBy)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function summarizePerson(person) {
|
|
115
|
+
if (!person || typeof person !== "object") {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const value = person;
|
|
119
|
+
return cleanObject({
|
|
120
|
+
id: value.id,
|
|
121
|
+
businessName: value.businessName,
|
|
122
|
+
email: value.email,
|
|
123
|
+
personType: value.personType,
|
|
124
|
+
profileType: value.profileType
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function truncate(value) {
|
|
128
|
+
if (typeof value !== "string") {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
132
|
+
if (normalized.length <= MAX_TEXT_LENGTH) {
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
return `${normalized.slice(0, MAX_TEXT_LENGTH)}... [truncated]`;
|
|
136
|
+
}
|
|
137
|
+
function stripHtml(value) {
|
|
138
|
+
return value
|
|
139
|
+
.replace(/<[^>]*>/g, " ")
|
|
140
|
+
.replace(/ /gi, " ")
|
|
141
|
+
.replace(/&/gi, "&")
|
|
142
|
+
.replace(/</gi, "<")
|
|
143
|
+
.replace(/>/gi, ">")
|
|
144
|
+
.replace(/"/gi, '"')
|
|
145
|
+
.replace(/'/g, "'");
|
|
146
|
+
}
|
|
147
|
+
function cleanObject(object) {
|
|
148
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined && value !== null));
|
|
149
|
+
}
|
|
150
|
+
function formatError(error) {
|
|
151
|
+
if (error instanceof MovideskApiError) {
|
|
152
|
+
return cleanObject({
|
|
153
|
+
type: error.name,
|
|
154
|
+
status: error.status,
|
|
155
|
+
message: error.message,
|
|
156
|
+
retryAfterSeconds: error.retryAfterSeconds
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
type: "UnexpectedError",
|
|
161
|
+
message: error instanceof Error ? error.message : "Erro desconhecido."
|
|
162
|
+
};
|
|
163
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-movidesk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server read-only para consultar tickets do Movidesk.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-movidesk": "./bin/mcp-movidesk.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"README.md",
|
|
14
|
+
".env.example"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "bun run src/index.ts",
|
|
18
|
+
"build": "tsc && bun run scripts/add-shebang.ts",
|
|
19
|
+
"prepack": "bun run build",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
24
|
+
"zod": "3.25.76"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bun": "1.3.13",
|
|
28
|
+
"typescript": "5.9.3"
|
|
29
|
+
}
|
|
30
|
+
}
|