feat: mcp-paperclip v1.0.0 — 165 tools para Paperclip AI

Triple transport (STDIO + StreamableHTTP + SSE porta 3175).
24 modulos: agents, issues, approvals, routines, goals, projects,
costs, activity, skills, secrets, plugins, assets, settings, access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 02:56:45 +01:00
commit 2753360787
43 changed files with 13071 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
PAPERCLIP_API_URL=https://clip.descomplicar.pt/api
PAPERCLIP_API_KEY=pcp_mcp_...
PAPERCLIP_COMPANY_ID=ebe10308-efd7-453f-86ab-13e6fe84004f
HTTP_PORT=3175
HTTP_HOST=127.0.0.1
LOG_LEVEL=error
LOG_FILE=logs/mcp-paperclip.log
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
logs/
*.log
+23
View File
@@ -0,0 +1,23 @@
# MCP Paperclip
MCP TypeScript para integrar Claude Code com Paperclip AI (clip.descomplicar.pt).
## Comandos
- `npm run build` — compilar TypeScript
- `npm run start` — iniciar STDIO transport
- `npm run start:http` — iniciar StreamableHTTP + SSE na porta 3175
- `npm run test` — correr testes
- `npm run quality:check` — lint + format + build + test
## Estrutura
- `src/client.ts` — HTTP client para API Paperclip
- `src/tools/*.ts` — tools organizadas por modulo (agents, issues, etc.)
- `src/server.ts` — factory createServer() partilhada entre transportes
- `src/index.ts` — entry STDIO
- `src/index-http.ts` — entry HTTP + SSE
## Convencoes
- Tools usam snake_case: `list_agents`, `create_issue`
- Company ID injectado automaticamente via env var
- Logs para stderr (nunca stdout em modo STDIO)
- Annotations inferidas automaticamente pelo prefixo do nome
+781
View File
@@ -0,0 +1,781 @@
---
title: "SPEC — MCP Paperclip v1.0"
date: 2026-04-07
type: spec
status: draft
project: mcp-paperclip
desk_task: ""
tags: [mcp, paperclip, typescript, agentes, orquestração]
---
# SPEC — MCP Paperclip v1.0
MCP TypeScript para integração completa com o Paperclip AI
(orquestrador autónomo em `clip.descomplicar.pt`).
Permite ao Claude Code gerir agentes, issues, rotinas, goals, projectos, aprovações,
custos, actividade, skills, segredos e dashboard via API REST.
---
## Contexto
| Item | Valor |
|------|-------|
| **API Base** | `https://clip.descomplicar.pt/api` (produção) |
| **Paperclip Version** | `0.3.1` |
| **Company ID** | `ebe10308-efd7-453f-86ab-13e6fe84004f` (Descomplicar) |
| **User ID** | `v1N5OccPn9DGq6iog7qW9nEvnXYFT3iO` (Emanuel) |
| **Auth** | `Authorization: Bearer {PAPERCLIP_API_KEY}` (Board API Key, SHA256 hash em `board_api_keys`) |
| **Destino** | `/home/ealmeida/mcp-servers/mcp-paperclip/` |
| **Transport** | STDIO (Claude Code) + StreamableHTTP porta `3175` + SSE porta `3175` (legado) |
| **Linguagem** | TypeScript (padrão Descomplicar) |
| **API Total** | 246 endpoints em 21 módulos — este MCP cobre 159 tools |
---
## Pré-requisito: Criar Board API Key
Antes da primeira utilização, gerar e registar uma Board API Key:
```bash
# Gerar key
KEY=$(node -e "console.log('pcp_mcp_' + require('crypto').randomBytes(24).toString('hex'))")
echo "PAPERCLIP_API_KEY=$KEY"
# Registar na BD (SHA256 hash)
HASH=$(node -e "const c=require('crypto'); process.argv[1] && console.log(c.createHash('sha256').update(process.argv[1]).digest('hex'))" "$KEY")
PGPASSWORD=paperclip psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c \
"INSERT INTO board_api_keys (user_id, name, key_hash) VALUES ('v1N5OccPn9DGq6iog7qW9nEvnXYFT3iO', 'claude-code-mcp', '$HASH');"
```
Guardar `KEY` no `.env` do MCP como `PAPERCLIP_API_KEY`.
---
## Arquitectura
```
mcp-paperclip/
├── src/
│ ├── index.ts # Entry STDIO (Claude Code)
│ ├── index-http.ts # Entry StreamableHTTP + SSE porta 3175
│ ├── server.ts # Lógica comum (createServer)
│ ├── client.ts # HTTP client para API Paperclip
│ ├── tools/
│ │ ├── index.ts # Registar todas as tools
│ │ ├── agents.ts # Agentes — CRUD, lifecycle, config, keys, heartbeats
│ │ ├── issues.ts # Issues — CRUD, comments, documents, work-products, labels
│ │ ├── routines.ts # Rotinas — CRUD, triggers, runs
│ │ ├── goals.ts # Goals — CRUD
│ │ ├── projects.ts # Projectos — CRUD, workspaces
│ │ ├── approvals.ts # Aprovações — governance workflow
│ │ ├── costs.ts # Custos e budgets — monitorização financeira
│ │ ├── activity.ts # Actividade — audit log
│ │ ├── skills.ts # Company skills — gestão de competências
│ │ ├── secrets.ts # Segredos — gestão de secrets encriptados
│ │ ├── plugins.ts # Plugins — instalar, activar, config, health
│ │ ├── assets.ts # Assets — upload imagens e logos
│ │ ├── settings.ts # Instance settings — configuração geral
│ │ ├── access.ts # Invites, join requests, members
│ │ ├── company.ts # Company, org, dashboard, members
│ │ └── health.ts # Health check
│ └── utils/
│ ├── logger.ts # Logger MCP-compatible (stderr, sem cores)
│ └── annotations.ts # inferAnnotations por prefixo
├── tests/
│ └── tools.test.ts
├── docs/
│ └── API_REFERENCE.md # Endpoints mapeados
├── scripts/
│ ├── validate-capabilities.sh
│ └── create-api-key.sh # Script criação Board API Key
├── .gitea/workflows/
│ └── ci.yml
├── .env.example
├── .eslintrc / eslint.config.js
├── .prettierrc
├── tsconfig.json
├── package.json
├── CLAUDE.md
├── CHANGELOG.md
└── README.md
```
---
## Tools (159 total)
### 1. Health (1 tool)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `get_health` | GET | `/health` | read |
Retorna: `{ status, version, deploymentMode, bootstrapStatus }`
---
### 2. Company / Org (10 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `get_company` | GET | `/companies/:id` | read |
| `list_companies` | GET | `/companies` | read |
| `get_company_stats` | GET | `/companies/stats` | read |
| `update_company` | PATCH | `/companies/:id` | write |
| `update_company_branding` | PATCH | `/companies/:id/branding` | write |
| `get_org_chart` | GET | `/companies/:id/org` | read |
| `get_dashboard` | GET | `/companies/:id/dashboard` | read |
| `list_members` | GET | `/companies/:id/members` | read |
| `update_member_permissions` | PATCH | `/companies/:id/members/:memberId/permissions` | write |
| `get_sidebar_badges` | GET | `/companies/:id/sidebar-badges` | read |
**Nota:** `get_org_chart` retorna JSON da hierarquia, não SVG.
---
### 3. Agentes (22 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_agents` | GET | `/companies/:id/agents` | read |
| `get_agent` | GET | `/agents/:id` | read |
| `get_agent_runtime_state` | GET | `/agents/:id/runtime-state` | read |
| `get_agent_configuration` | GET | `/agents/:id/configuration` | read |
| `get_agent_config_revisions` | GET | `/agents/:id/config-revisions` | read |
| `rollback_agent_config` | POST | `/agents/:id/config-revisions/:revisionId/rollback` | write |
| `get_agent_skills` | GET | `/agents/:id/skills` | read |
| `sync_agent_skills` | POST | `/agents/:id/skills/sync` | write |
| `get_agent_task_sessions` | GET | `/agents/:id/task-sessions` | read |
| `get_agent_instructions_bundle` | GET | `/agents/:id/instructions-bundle` | read |
| `update_agent_instructions_bundle` | PATCH | `/agents/:id/instructions-bundle` | write |
| `create_agent` | POST | `/companies/:id/agents` | write |
| `create_agent_hire` | POST | `/companies/:id/agent-hires` | write |
| `update_agent` | PATCH | `/agents/:id` | write |
| `update_agent_permissions` | PATCH | `/agents/:id/permissions` | write |
| `update_agent_instructions_path` | PATCH | `/agents/:id/instructions-path` | write |
| `pause_agent` | POST | `/agents/:id/pause` | write |
| `resume_agent` | POST | `/agents/:id/resume` | write |
| `wakeup_agent` | POST | `/agents/:id/wakeup` | write |
| `invoke_agent_heartbeat` | POST | `/agents/:id/heartbeat/invoke` | write |
| `terminate_agent` | POST | `/agents/:id/terminate` | destrutivo |
| `delete_agent` | DELETE | `/agents/:id` | destrutivo |
**Parâmetros `create_agent`:**
```typescript
{
name: string, // nome do agente
role: string, // ceo | coo | cto | engineer | analyst | devops | pm | ...
title?: string, // descrição do cargo
reports_to?: string, // UUID do agente supervisor
capabilities?: string, // texto livre
model?: string, // claude-sonnet-4-6 | claude-opus-4-6 | ...
budget_monthly_cents?: number,
instructions_file_path?: string // path absoluto para AGENTS.md
}
```
**Parâmetros `create_agent_hire`:**
```typescript
{
name: string,
role: string,
title?: string,
reports_to?: string,
// Passa por governance/approval antes de criar o agente
}
```
**Parâmetros `wakeup_agent`:**
```typescript
{
agent_id: string,
issue_id?: string, // acordar em contexto de issue específica
message?: string // mensagem de contexto
}
```
---
### 4. Agent Keys (3 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_agent_keys` | GET | `/agents/:id/keys` | read |
| `create_agent_key` | POST | `/agents/:id/keys` | write |
| `delete_agent_key` | DELETE | `/agents/:id/keys/:keyId` | destrutivo |
---
### 5. Heartbeat Runs (6 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_heartbeat_runs` | GET | `/companies/:id/heartbeat-runs` | read |
| `list_live_runs` | GET | `/companies/:id/live-runs` | read |
| `get_heartbeat_run` | GET | `/heartbeat-runs/:runId` | read |
| `get_heartbeat_run_events` | GET | `/heartbeat-runs/:runId/events` | read |
| `get_heartbeat_run_log` | GET | `/heartbeat-runs/:runId/log` | read |
| `cancel_heartbeat_run` | POST | `/heartbeat-runs/:runId/cancel` | write |
---
### 6. Issues (17 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_issues` | GET | `/companies/:id/issues` | read |
| `get_issue` | GET | `/issues/:id` | read |
| `get_issue_heartbeat_context` | GET | `/issues/:id/heartbeat-context` | read |
| `create_issue` | POST | `/companies/:id/issues` | write |
| `update_issue` | PATCH | `/issues/:id` | write |
| `delete_issue` | DELETE | `/issues/:id` | destrutivo |
| `checkout_issue` | POST | `/issues/:id/checkout` | write |
| `release_issue` | POST | `/issues/:id/release` | write |
| `get_issue_comments` | GET | `/issues/:id/comments` | read |
| `add_issue_comment` | POST | `/issues/:id/comments` | write |
| `list_issue_documents` | GET | `/issues/:id/documents` | read |
| `get_issue_document` | GET | `/issues/:id/documents/:key` | read |
| `upsert_issue_document` | PUT | `/issues/:id/documents/:key` | write |
| `delete_issue_document` | DELETE | `/issues/:id/documents/:key` | destrutivo |
| `list_issue_work_products` | GET | `/issues/:id/work-products` | read |
| `create_issue_work_product` | POST | `/issues/:id/work-products` | write |
| `list_issue_live_runs` | GET | `/issues/:id/live-runs` | read |
**Parâmetros `create_issue`:**
```typescript
{
title: string,
description?: string, // markdown
assignee_id?: string, // UUID do agente
priority?: 'low' | 'medium' | 'high' | 'urgent',
labels?: string[]
}
```
**Nota:** Issues são a forma principal de dar trabalho aos agentes.
`checkout_issue` -> agente bloqueia e começa a trabalhar.
`release_issue` -> agente liberta e fica idle.
---
### 7. Labels (3 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_labels` | GET | `/companies/:id/labels` | read |
| `create_label` | POST | `/companies/:id/labels` | write |
| `delete_label` | DELETE | `/labels/:labelId` | destrutivo |
---
### 8. Issue Attachments (4 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_issue_attachments` | GET | `/issues/:id/attachments` | read |
| `upload_issue_attachment` | POST | `/companies/:id/issues/:issueId/attachments` | write |
| `get_attachment_content` | GET | `/attachments/:attachmentId/content` | read |
| `delete_attachment` | DELETE | `/attachments/:attachmentId` | destrutivo |
---
### 9. Approvals / Governance (10 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_approvals` | GET | `/companies/:id/approvals` | read |
| `get_approval` | GET | `/approvals/:id` | read |
| `create_approval` | POST | `/companies/:id/approvals` | write |
| `list_approval_issues` | GET | `/approvals/:id/issues` | read |
| `approve_approval` | POST | `/approvals/:id/approve` | write |
| `reject_approval` | POST | `/approvals/:id/reject` | write |
| `request_approval_revision` | POST | `/approvals/:id/request-revision` | write |
| `resubmit_approval` | POST | `/approvals/:id/resubmit` | write |
| `list_approval_comments` | GET | `/approvals/:id/comments` | read |
| `add_approval_comment` | POST | `/approvals/:id/comments` | write |
---
### 10. Rotinas (8 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_routines` | GET | `/companies/:id/routines` | read |
| `get_routine` | GET | `/routines/:id` | read |
| `create_routine` | POST | `/companies/:id/routines` | write |
| `update_routine` | PATCH | `/routines/:id` | write |
| `list_routine_runs` | GET | `/routines/:id/runs` | read |
| `run_routine` | POST | `/routines/:id/run` | write |
| `create_routine_trigger` | POST | `/routines/:id/triggers` | write |
| `delete_routine_trigger` | DELETE | `/routine-triggers/:id` | destrutivo |
---
### 11. Goals (5 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_goals` | GET | `/companies/:id/goals` | read |
| `get_goal` | GET | `/goals/:id` | read |
| `create_goal` | POST | `/companies/:id/goals` | write |
| `update_goal` | PATCH | `/goals/:id` | write |
| `delete_goal` | DELETE | `/goals/:id` | destrutivo |
---
### 12. Projectos (7 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_projects` | GET | `/companies/:id/projects` | read |
| `get_project` | GET | `/projects/:id` | read |
| `create_project` | POST | `/companies/:id/projects` | write |
| `update_project` | PATCH | `/projects/:id` | write |
| `delete_project` | DELETE | `/projects/:id` | destrutivo |
| `list_project_workspaces` | GET | `/projects/:id/workspaces` | read |
| `create_project_workspace` | POST | `/projects/:id/workspaces` | write |
---
### 13. Costs / Budgets (12 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `get_cost_summary` | GET | `/companies/:id/costs/summary` | read |
| `get_costs_by_agent` | GET | `/companies/:id/costs/by-agent` | read |
| `get_costs_by_agent_model` | GET | `/companies/:id/costs/by-agent-model` | read |
| `get_costs_by_provider` | GET | `/companies/:id/costs/by-provider` | read |
| `get_costs_by_project` | GET | `/companies/:id/costs/by-project` | read |
| `get_finance_summary` | GET | `/companies/:id/costs/finance-summary` | read |
| `list_finance_events` | GET | `/companies/:id/costs/finance-events` | read |
| `get_window_spend` | GET | `/companies/:id/costs/window-spend` | read |
| `get_quota_windows` | GET | `/companies/:id/costs/quota-windows` | read |
| `get_budgets_overview` | GET | `/companies/:id/budgets/overview` | read |
| `update_company_budget` | PATCH | `/companies/:id/budgets` | write |
| `update_agent_budget` | PATCH | `/agents/:agentId/budgets` | write |
---
### 14. Activity / Audit Log (4 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_company_activity` | GET | `/companies/:id/activity` | read |
| `create_activity_entry` | POST | `/companies/:id/activity` | write |
| `list_issue_activity` | GET | `/issues/:id/activity` | read |
| `list_issue_runs` | GET | `/issues/:id/runs` | read |
---
### 15. Company Skills (6 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_company_skills` | GET | `/companies/:id/skills` | read |
| `get_company_skill` | GET | `/companies/:id/skills/:skillId` | read |
| `list_skill_files` | GET | `/companies/:id/skills/:skillId/files` | read |
| `create_company_skill` | POST | `/companies/:id/skills` | write |
| `import_company_skill` | POST | `/companies/:id/skills/import` | write |
| `delete_company_skill` | DELETE | `/companies/:id/skills/:skillId` | destrutivo |
---
### 16. Secrets (5 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_secrets` | GET | `/companies/:id/secrets` | read |
| `list_secret_providers` | GET | `/companies/:id/secret-providers` | read |
| `create_secret` | POST | `/companies/:id/secrets` | write |
| `rotate_secret` | POST | `/secrets/:id/rotate` | write |
| `delete_secret` | DELETE | `/secrets/:id` | destrutivo |
---
### 17. Execution Workspaces (3 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_execution_workspaces` | GET | `/companies/:id/execution-workspaces` | read |
| `get_execution_workspace` | GET | `/execution-workspaces/:id` | read |
| `update_execution_workspace` | PATCH | `/execution-workspaces/:id` | write |
---
### 18. Adapters / Models (2 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_adapter_models` | GET | `/companies/:id/adapters/:type/models` | read |
| `test_adapter_environment` | POST | `/companies/:id/adapters/:type/test-environment` | write |
---
### 19. Portability / Export-Import (2 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `export_company` | POST | `/companies/:id/exports` | write |
| `import_company_preview` | POST | `/companies/:id/imports/preview` | read |
---
### 20. Plugins (17 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `list_plugins` | GET | `/plugins` | read |
| `get_plugin` | GET | `/plugins/:pluginId` | read |
| `list_plugin_examples` | GET | `/plugins/examples` | read |
| `list_plugin_tools` | GET | `/plugins/tools` | read |
| `install_plugin` | POST | `/plugins/install` | write |
| `delete_plugin` | DELETE | `/plugins/:pluginId` | destrutivo |
| `enable_plugin` | POST | `/plugins/:pluginId/enable` | write |
| `disable_plugin` | POST | `/plugins/:pluginId/disable` | write |
| `upgrade_plugin` | POST | `/plugins/:pluginId/upgrade` | write |
| `get_plugin_health` | GET | `/plugins/:pluginId/health` | read |
| `get_plugin_logs` | GET | `/plugins/:pluginId/logs` | read |
| `get_plugin_config` | GET | `/plugins/:pluginId/config` | read |
| `set_plugin_config` | POST | `/plugins/:pluginId/config` | write |
| `test_plugin_config` | POST | `/plugins/:pluginId/config/test` | write |
| `get_plugin_dashboard` | GET | `/plugins/:pluginId/dashboard` | read |
| `list_plugin_jobs` | GET | `/plugins/:pluginId/jobs` | read |
| `trigger_plugin_job` | POST | `/plugins/:pluginId/jobs/:jobId/trigger` | write |
---
### 21. Assets (3 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `upload_image_asset` | POST | `/companies/:id/assets/images` | write |
| `upload_company_logo` | POST | `/companies/:id/logo` | write |
| `get_asset_content` | GET | `/assets/:assetId/content` | read |
**Nota:** Uploads usam `multipart/form-data`.
---
### 22. Instance Settings (4 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `get_general_settings` | GET | `/instance/settings/general` | read |
| `update_general_settings` | PATCH | `/instance/settings/general` | write |
| `get_experimental_settings` | GET | `/instance/settings/experimental` | read |
| `update_experimental_settings` | PATCH | `/instance/settings/experimental` | write |
---
### 23. Invites / Onboarding (7 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `create_invite` | POST | `/companies/:id/invites` | write |
| `get_invite` | GET | `/invites/:token` | read |
| `get_invite_onboarding` | GET | `/invites/:token/onboarding` | read |
| `accept_invite` | POST | `/invites/:token/accept` | write |
| `revoke_invite` | POST | `/invites/:inviteId/revoke` | write |
| `list_join_requests` | GET | `/companies/:id/join-requests` | read |
| `approve_join_request` | POST | `/companies/:id/join-requests/:requestId/approve` | write |
---
### 24. Plugin Bridge / Execution (4 tools)
| Tool | Método | Endpoint | Tipo |
|------|--------|----------|------|
| `execute_plugin_tool` | POST | `/plugins/tools/execute` | write |
| `plugin_bridge_data` | POST | `/plugins/:pluginId/bridge/data` | read |
| `plugin_bridge_action` | POST | `/plugins/:pluginId/bridge/action` | write |
| `list_ui_contributions` | GET | `/plugins/ui-contributions` | read |
---
## Endpoints Excluídos (não implementados como tools)
| Módulo | Razão |
|--------|-------|
| **CLI Auth** (challenges, approve, cancel, revoke) | Fluxo interactivo browser-based — não adequado para MCP |
| **Board Claims** (get/claim token) | Fluxo de setup inicial — uma vez por instalação |
| **LLMs** (`/llms/*.txt`) | Documentação estática — aceder directamente via HTTP |
| **Plugin SSE Stream** (`/plugins/:id/bridge/stream/:channel`) | SSE stream contínuo — não compatível com request/response MCP |
| **Admin Operations** (promote/demote instance admin, user company access) | Operações privilegiadas de super-admin — proteger de uso acidental |
---
## Variáveis de Ambiente
```bash
# .env.example
PAPERCLIP_API_URL=https://clip.descomplicar.pt/api
PAPERCLIP_API_KEY=pcp_mcp_... # Board API Key gerada pelo script
PAPERCLIP_COMPANY_ID=ebe10308-efd7-453f-86ab-13e6fe84004f
# HTTP/SSE transport
HTTP_PORT=3175
```
**`PAPERCLIP_COMPANY_ID` injectado automaticamente** nas tools que precisam de `:companyId`,
sem obrigar o utilizador a passá-lo em cada chamada.
---
## Cliente HTTP (`src/client.ts`)
```typescript
export class PaperclipClient {
private baseUrl: string;
private companyId: string;
private headers: Record<string, string>;
constructor() {
this.baseUrl = process.env.PAPERCLIP_API_URL ?? 'https://clip.descomplicar.pt/api';
this.companyId = process.env.PAPERCLIP_COMPANY_ID ?? '';
this.headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PAPERCLIP_API_KEY}`,
};
}
async get<T>(path: string, params?: Record<string, string>): Promise<T>
async post<T>(path: string, body?: unknown): Promise<T>
async patch<T>(path: string, body?: unknown): Promise<T>
async put<T>(path: string, body?: unknown): Promise<T>
async delete<T>(path: string): Promise<T>
// Helpers com company_id injectado
companyPath(suffix: string): string {
return `/companies/${this.companyId}${suffix}`;
}
}
```
---
## Tool Annotations
```typescript
// Aplicar inferAnnotations() a todas as tools:
// list_*, get_* → readOnlyHint: true
// delete_*, terminate_* → destructiveHint: true
// update_*, patch_* → idempotentHint: true
// create_*, add_*, checkout_*, release_*, approve_*, reject_* → write normal
// run_*, invoke_*, wakeup_*, fire_* → write (side-effects)
```
---
## Configuração `~/.claude.json`
```json
{
"mcpServers": {
"paperclip": {
"command": "node",
"args": ["/home/ealmeida/mcp-servers/mcp-paperclip/dist/index.js"],
"env": {
"PAPERCLIP_API_URL": "https://clip.descomplicar.pt/api",
"PAPERCLIP_API_KEY": "pcp_mcp_...",
"PAPERCLIP_COMPANY_ID": "ebe10308-efd7-453f-86ab-13e6fe84004f"
}
}
}
}
```
---
## Plano de Implementação (6 sprints)
### Sprint 1 — Fundação + Health + Agentes (read-only)
**Objectivo:** MCP operacional com tools de leitura base
- [ ] `npm init` + dependências (`@modelcontextprotocol/sdk`, `zod`, `dotenv`, `winston`)
- [ ] DevDeps: TypeScript, ESLint, Prettier, Jest, Husky
- [ ] `tsconfig.json`, `eslint.config.js`, `.prettierrc`, `.gitignore`
- [ ] `src/client.ts` — PaperclipClient com auth Bearer e métodos GET/POST/PATCH/PUT/DELETE
- [ ] `src/utils/logger.ts` — logger MCP-compatible (stderr, sem cores)
- [ ] `src/utils/annotations.ts` — inferAnnotations
- [ ] `src/server.ts` — createServer com capabilities completas
- [ ] `src/index.ts` — entry STDIO
- [ ] `src/index-http.ts` — entry StreamableHTTP + SSE porta 3175 (stateless)
- [ ] `scripts/create-api-key.sh` — script criação Board API Key
- [ ] Tools `health.ts`: `get_health`
- [ ] Tools `company.ts`: `get_company`, `list_companies`, `get_company_stats`, `get_org_chart`, `get_dashboard`, `get_sidebar_badges`
- [ ] Tools `agents.ts` (read): `list_agents`, `get_agent`, `get_agent_runtime_state`, `get_agent_configuration`, `get_agent_config_revisions`, `get_agent_skills`, `get_agent_task_sessions`, `get_agent_instructions_bundle`
- [ ] `.env.example`, `CLAUDE.md`, `CHANGELOG.md`, `README.md`
- [ ] Testes básicos das tools read
- [ ] `npm run quality:check` passa
- [ ] Repo Gitea criado, CI verde
**Critério de aceitação Sprint 1:**
`list_agents` devolve os 64 agentes da Descomplicar via STDIO.
---
### Sprint 2 — Issues + Write Operations Agentes
**Objectivo:** CRUD completo de issues e operações write em agentes
- [ ] Tools `issues.ts`: list, get, create, update, delete, comments, checkout, release, documents, work-products, heartbeat-context, live-runs (17 tools)
- [ ] Tools `labels.ts`: list, create, delete (3 tools)
- [ ] Tools `attachments.ts`: list, upload, get_content, delete (4 tools)
- [ ] Tools `agents.ts` (write): create, create_hire, update, update_permissions, update_instructions_path, update_instructions_bundle, rollback_config, sync_skills, pause, resume, wakeup, invoke_heartbeat, terminate, delete (14 tools)
- [ ] Tools `agent_keys.ts`: list, create, delete (3 tools)
- [ ] Validação Zod em inputs de write
- [ ] Tratamento de erros HTTP (401, 403, 404, 422) -> mensagens úteis
- [ ] Testes issues e write ops
- [ ] Actualizar `~/.claude.json` com MCP configurado
- [ ] Smoke test: criar issue, atribuir ao Reality Checker, fazer checkout
**Critério de aceitação Sprint 2:**
Claude Code consegue criar uma issue, assignar a um agente e fazer checkout via MCP.
---
### Sprint 3 — Routines, Goals, Projects, Approvals
**Objectivo:** Workflows completos de gestão e governance
- [ ] Tools `routines.ts`: list, get, create, update, list_runs, run, create_trigger, delete_trigger (8 tools)
- [ ] Tools `goals.ts`: list, get, create, update, delete (5 tools)
- [ ] Tools `projects.ts`: list, get, create, update, delete, list_workspaces, create_workspace (7 tools)
- [ ] Tools `approvals.ts`: list, get, create, list_issues, approve, reject, request_revision, resubmit, comments (10 tools)
- [ ] Testes de workflows: criar goal -> criar issue -> aprovar -> executar
**Critério de aceitação Sprint 3:**
Workflow completo de governance: criar approval, aprovar, verificar estado.
---
### Sprint 4 — Costs, Activity, Skills, Secrets, Workspaces
**Objectivo:** Observabilidade e gestão operacional
- [ ] Tools `costs.ts`: summary, by-agent, by-model, by-provider, by-project, finance, budgets (12 tools)
- [ ] Tools `activity.ts`: company activity, issue activity, issue runs (4 tools)
- [ ] Tools `skills.ts`: list, get, files, create, import, delete (6 tools)
- [ ] Tools `secrets.ts`: list, providers, create, rotate, delete (5 tools)
- [ ] Tools: execution_workspaces (3 tools), adapters (2 tools), portability (2 tools)
- [ ] Tools `company.ts` (write): update, update_branding, members, update_member_permissions
- [ ] Heartbeat runs: list, live, get, events, log, cancel (6 tools)
- [ ] Testes de observabilidade
**Critério de aceitação Sprint 4:**
`get_cost_summary` + `get_budgets_overview` retornam dados reais.
---
### Sprint 5 — Plugins, Assets, Settings, Access
**Objectivo:** Gestão de plugins, assets, configuração e onboarding
- [ ] Tools `plugins.ts`: list, get, examples, tools, install, delete, enable, disable, upgrade, health, logs, config, dashboard, jobs, trigger (17 tools)
- [ ] Tools `plugin_bridge.ts`: execute_tool, bridge_data, bridge_action, ui_contributions (4 tools)
- [ ] Tools `assets.ts`: upload_image, upload_logo, get_content (3 tools) — suporte multipart/form-data no client
- [ ] Tools `settings.ts`: get/update general, get/update experimental (4 tools)
- [ ] Tools `access.ts`: create_invite, get_invite, get_onboarding, accept_invite, revoke_invite, list_join_requests, approve_join_request (7 tools)
- [ ] Testes de plugins e settings
**Critério de aceitação Sprint 5:**
`list_plugins` + `install_plugin` + `get_plugin_health` funcionais.
---
### Sprint 6 — Gateway Deploy + CI/CD + Documentação
**Objectivo:** Deploy produção + cobertura total
- [ ] Deploy no gateway (5.9.90.69) via PM2
- [ ] Registar porta 3175 no `port-map.json` do gateway
- [ ] CI/CD workflow Gitea (`.gitea/workflows/deploy.yml`)
- [ ] Actualizar `~/.claude/_resources/mcps.json`
- [ ] Actualizar `QR-Servidores.md` com novo MCP
- [ ] `docs/API_REFERENCE.md` completo
- [ ] Testes integração end-to-end (todas as 159 tools)
- [ ] Smoke test final: workflow completo via gateway StreamableHTTP
**Critério de aceitação Sprint 6:**
Todas as 159 tools operacionais via STDIO + StreamableHTTP + SSE. CI verde.
---
## Critérios de Qualidade (pré-entrega)
```
[ ] Zero erros ESLint
[ ] Testes passam: npm run test
[ ] Build sem erros: npm run build
[ ] Capabilities completas (tools + resources + prompts)
[ ] Nomenclatura snake_case em todas as tools
[ ] Triple transport funcional (STDIO + StreamableHTTP + SSE na porta 3175)
[ ] Stateless (sessionIdGenerator: undefined)
[ ] Logger para stderr (nunca stdout em STDIO mode)
[ ] .env.example sem valores reais
[ ] CHANGELOG.md iniciado
[ ] CI verde no Gitea
[ ] 159 tools operacionais
```
---
## Dependências
```json
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.0",
"dotenv": "^16.4.7",
"winston": "^3.17.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.1",
"jest": "^30.0.5",
"prettier": "^3.6.2",
"ts-jest": "^29.4.0",
"typescript": "^5.3.3"
}
}
```
---
## Notas de Implementação
1. **`PAPERCLIP_COMPANY_ID` como constante** — injectar em todas as tools que precisam de `:companyId`, nunca expor como parâmetro obrigatório ao utilizador.
2. **Erros da API Paperclip** — mapear para mensagens claras:
- `401/403` -> "Sem autorização. Verificar `PAPERCLIP_API_KEY`."
- `404` -> "Recurso não encontrado: {id}"
- `422` -> incluir erros de validação da resposta
3. **`get_org_chart`** — retorna JSON da hierarquia de agentes, não o SVG. Usar `/companies/:id/org` (JSON) e não `/org.svg`.
4. **`wakeup_agent`** — o Paperclip acorda agentes via heartbeat; útil para forçar execução de uma rotina ou atribuir uma issue urgente.
5. **`checkout_issue`** — permite ao Claude Code "reservar" uma issue enquanto trabalha nela, sinalizando que está em curso.
6. **`create_agent_hire`** — usa governance/approval workflow. Preferível a `create_agent` directo quando há supervisão activa.
7. **Idempotência**`create_agent` com mesmo `name` pode criar duplicado. Adicionar verificação prévia com `list_agents` se necessário.
8. **Costs** — endpoints de custos aceitam query params `from`, `to` (ISO dates) para filtragem temporal.
9. **Secrets** — valores nunca são retornados em plain text. Apenas metadados (nome, provider, criação).
10. **Attachments e Assets** — uploads usam `multipart/form-data`. O client precisa de suportar este content-type para `upload_issue_attachment`, `upload_image_asset` e `upload_company_logo`.
11. **Plugins**`install_plugin` aceita URL ou path para o plugin. `execute_plugin_tool` permite invocar tools expostas por plugins instalados.
12. **Instance Settings** — alterações a `experimental` podem activar/desactivar funcionalidades em beta. Usar com cautela.
13. **Invites**`create_invite` gera token único para onboarding de novos agentes. O fluxo completo: create -> partilhar token -> accept.
14. **Plugin Bridge**`plugin_bridge_data` e `plugin_bridge_action` permitem comunicação directa com plugins via data/action pattern.
---
*SPEC criado em 07-04-2026 | Actualizado em 07-04-2026 (cobertura completa API v0.3.1)*
*Baseado no PROC-MCP-Desenvolvimento.md v2.5 + análise directa do código-fonte Paperclip*
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', { useESM: true }],
},
};
+7169
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "mcp-paperclip",
"version": "1.0.0",
"description": "MCP Server para Paperclip AI — gestao de agentes, issues, rotinas e governance",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"start:http": "node dist/index-http.js",
"dev": "tsc --watch",
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"quality:check": "npm run lint && npm run format:check && npm run build && npm run test"
},
"author": "Descomplicar",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"dotenv": "^16.6.1",
"winston": "^3.19.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^22.19.17",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.1",
"jest": "^30.3.0",
"prettier": "^3.8.1",
"ts-jest": "^29.4.9",
"typescript": "^5.9.3"
}
}
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Gerar e registar Board API Key para MCP Paperclip
set -euo pipefail
KEY=$(node -e "console.log('pcp_mcp_' + require('crypto').randomBytes(24).toString('hex'))")
HASH=$(node -e "const c=require('crypto'); console.log(c.createHash('sha256').update(process.argv[1]).digest('hex'))" "$KEY")
echo "=== MCP Paperclip API Key ==="
echo "PAPERCLIP_API_KEY=$KEY"
echo ""
echo "Registar na BD:"
echo "PGPASSWORD=paperclip psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c \\"
echo " \"INSERT INTO board_api_keys (user_id, name, key_hash) VALUES ('v1N5OccPn9DGq6iog7qW9nEvnXYFT3iO', 'claude-code-mcp', '$HASH');\""
+88
View File
@@ -0,0 +1,88 @@
import { logger } from './utils/logger.js';
export class PaperclipClient {
private baseUrl: string;
private companyId: string;
private headers: Record<string, string>;
constructor() {
this.baseUrl = process.env.PAPERCLIP_API_URL ?? 'https://clip.descomplicar.pt/api';
this.companyId = process.env.PAPERCLIP_COMPANY_ID ?? '';
if (!process.env.PAPERCLIP_API_KEY) {
logger.warn('PAPERCLIP_API_KEY not set');
}
this.headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.PAPERCLIP_API_KEY ?? ''}`,
};
}
companyPath(suffix: string): string {
return `/companies/${this.companyId}${suffix}`;
}
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
const url = `${this.baseUrl}${path}`;
const options: RequestInit = { method, headers: this.headers };
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
await this.handleError(response, path);
}
return response.json() as Promise<T>;
}
private async handleError(response: Response, path: string): Promise<never> {
const status = response.status;
if (status === 401 || status === 403) {
throw new Error('Sem autorização. Verificar PAPERCLIP_API_KEY.');
}
if (status === 404) {
throw new Error(`Recurso não encontrado: ${path}`);
}
let detail = '';
try {
const body = await response.json();
detail = JSON.stringify(body);
} catch {
detail = response.statusText;
}
if (status === 422) {
throw new Error(`Erro de validação: ${detail}`);
}
throw new Error(`Erro API (${status}): ${detail}`);
}
async get<T>(path: string): Promise<T> {
return this.request<T>('GET', path);
}
async post<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('POST', path, body);
}
async patch<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('PATCH', path, body);
}
async put<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('PUT', path, body);
}
async delete<T>(path: string): Promise<T> {
return this.request<T>('DELETE', path);
}
}
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env node
/**
* MCP Paperclip - HTTP Server Mode (Streamable HTTP + SSE)
* Dual transport: StreamableHTTP (primary) + SSE (legacy compatibility)
*
* @author Descomplicar | @link descomplicar.pt | @copyright 2026
*/
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import * as dotenv from 'dotenv';
import * as http from 'http';
import { randomUUID } from 'crypto';
import { createServer } from './server.js';
import { allTools } from './tools/index.js';
import { logger } from './utils/logger.js';
dotenv.config();
const PORT = parseInt(process.env.HTTP_PORT || '3175');
const HOST = process.env.HTTP_HOST || '127.0.0.1';
// Track active StreamableHTTP sessions
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
// Track SSE sessions
const sseSessions = new Map<string, SSEServerTransport>();
async function main() {
const httpServer = http.createServer(async (req, res) => {
// CORS headers on all responses
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
const url = new URL(req.url || '/', `http://${HOST}:${PORT}`);
// Health check endpoint
if (url.pathname === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
status: 'ok',
transport: 'http+sse',
sessions: sessions.size,
sse_sessions: sseSessions.size,
tools: allTools.length,
})
);
return;
}
// StreamableHTTP endpoint — POST/DELETE
if (url.pathname === '/mcp') {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
// Route to existing session
if (sessionId && sessions.has(sessionId)) {
const session = sessions.get(sessionId)!;
await session.transport.handleRequest(req, res);
return;
}
// DELETE — session termination
if (req.method === 'DELETE' && sessionId) {
const session = sessions.get(sessionId);
if (session) {
await session.transport.close();
sessions.delete(sessionId);
res.writeHead(200);
res.end();
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Session not found' }));
}
return;
}
// POST — new session
if (req.method === 'POST') {
const server = createServer(allTools);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true,
onsessioninitialized: (newSessionId) => {
sessions.set(newSessionId, { transport });
logger.info(`HTTP session initialized: ${newSessionId}`);
},
});
transport.onclose = () => {
if (transport.sessionId) {
logger.info(`HTTP session closed: ${transport.sessionId}`);
sessions.delete(transport.sessionId);
}
};
await server.connect(transport);
await transport.handleRequest(req, res);
return;
}
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
// SSE endpoint — GET (legacy transport)
if (url.pathname === '/sse' && req.method === 'GET') {
const server = createServer(allTools);
const transport = new SSEServerTransport('/message', res);
sseSessions.set(transport.sessionId, transport);
logger.info(`SSE session started: ${transport.sessionId}`);
transport.onclose = () => {
logger.info(`SSE session closed: ${transport.sessionId}`);
sseSessions.delete(transport.sessionId);
};
await server.connect(transport);
return;
}
// SSE message endpoint — POST
if (url.pathname === '/message' && req.method === 'POST') {
const sessionId = url.searchParams.get('sessionId');
if (!sessionId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing sessionId query param' }));
return;
}
const transport = sseSessions.get(sessionId);
if (!transport) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'SSE session not found' }));
return;
}
await transport.handlePostMessage(req, res);
return;
}
// 404 for all other paths
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found', hint: 'Use /mcp (StreamableHTTP) or /sse (SSE)' }));
});
httpServer.listen(PORT, HOST, () => {
logger.info('MCP Paperclip HTTP Server started', {
host: HOST,
port: PORT,
tools: allTools.length,
endpoints: {
health: `http://${HOST}:${PORT}/health`,
mcp: `http://${HOST}:${PORT}/mcp`,
sse: `http://${HOST}:${PORT}/sse`,
},
});
console.log(`MCP Paperclip HTTP Server running at http://${HOST}:${PORT}/mcp`);
});
// Graceful shutdown
const shutdown = (signal: string) => {
logger.info(`${signal} received, shutting down...`);
for (const session of sessions.values()) {
session.transport.close();
}
sessions.clear();
for (const transport of sseSessions.values()) {
transport.close();
}
sseSessions.clear();
httpServer.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
main().catch((error) => {
logger.error('Fatal error', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
process.exit(1);
});
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as dotenv from 'dotenv';
import { createServer } from './server.js';
import { allTools } from './tools/index.js';
import { logger } from './utils/logger.js';
dotenv.config();
async function main() {
const server = createServer(allTools);
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('MCP Paperclip STDIO started', { tools: allTools.length });
}
main().catch((error) => {
logger.error('Fatal error', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
process.exit(1);
});
+68
View File
@@ -0,0 +1,68 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { PaperclipTool } from './types.js';
import { inferAnnotations } from './utils/annotations.js';
import { logger } from './utils/logger.js';
export function createServer(allTools: PaperclipTool[]): Server {
const server = new Server({
name: 'mcp-paperclip',
version: '1.0.0',
});
(server as any)._capabilities = {
tools: {},
resources: {},
prompts: {},
};
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
annotations: inferAnnotations(tool.name),
})),
}));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [],
}));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = allTools.find((t) => t.name === name);
if (!tool) {
return {
content: [{ type: 'text', text: `Tool '${name}' não encontrada` }],
};
}
try {
return await tool.handler(args as Record<string, unknown>);
} catch (error) {
logger.error(`Erro na tool ${name}:`, error);
return {
content: [
{
type: 'text',
text: `Erro na tool ${name}: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
});
return server;
}
+113
View File
@@ -0,0 +1,113 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const accessTools: PaperclipTool[] = [
{
name: 'create_invite',
description: 'Criar um convite para um agente na empresa',
inputSchema: {
type: 'object',
properties: {
agent_name: { type: 'string', description: 'Nome do agente convidado' },
role: { type: 'string', description: 'Papel do agente (opcional)' },
instructions: { type: 'string', description: 'Instruções para o agente (opcional)' },
},
required: ['agent_name'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/invites'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_invite',
description: 'Obter detalhes de um convite pelo token',
inputSchema: {
type: 'object',
properties: {
token: { type: 'string', description: 'Token do convite' },
},
required: ['token'],
},
handler: async (args) => {
const { token } = args as { token: string };
const result = await client.get(`/invites/${token}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_invite_onboarding',
description: 'Obter informações de onboarding de um convite pelo token',
inputSchema: {
type: 'object',
properties: {
token: { type: 'string', description: 'Token do convite' },
},
required: ['token'],
},
handler: async (args) => {
const { token } = args as { token: string };
const result = await client.get(`/invites/${token}/onboarding`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'accept_invite',
description: 'Aceitar um convite pelo token',
inputSchema: {
type: 'object',
properties: {
token: { type: 'string', description: 'Token do convite' },
},
required: ['token'],
},
handler: async (args) => {
const { token } = args as { token: string };
const result = await client.post(`/invites/${token}/accept`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'revoke_invite',
description: 'Revogar um convite pelo ID',
inputSchema: {
type: 'object',
properties: {
invite_id: { type: 'string', description: 'ID do convite' },
},
required: ['invite_id'],
},
handler: async (args) => {
const { invite_id } = args as { invite_id: string };
const result = await client.post(`/invites/${invite_id}/revoke`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_join_requests',
description: 'Listar pedidos de adesão à empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/join-requests'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'approve_join_request',
description: 'Aprovar um pedido de adesão pelo ID',
inputSchema: {
type: 'object',
properties: {
request_id: { type: 'string', description: 'ID do pedido de adesão' },
},
required: ['request_id'],
},
handler: async (args) => {
const { request_id } = args as { request_id: string };
const result = await client.post(client.companyPath(`/join-requests/${request_id}/approve`));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+64
View File
@@ -0,0 +1,64 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const activityTools: PaperclipTool[] = [
{
name: 'list_company_activity',
description: 'Listar actividade da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/activity'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_activity_entry',
description: 'Criar uma entrada de actividade na empresa',
inputSchema: {
type: 'object',
properties: {
type: { type: 'string', description: 'Tipo de actividade' },
description: { type: 'string', description: 'Descricao da actividade' },
},
required: ['type', 'description'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/activity'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_issue_activity',
description: 'Listar actividade de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/activity`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_issue_runs',
description: 'Listar execucoes de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/runs`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+39
View File
@@ -0,0 +1,39 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const adapterTools: PaperclipTool[] = [
{
name: 'list_adapter_models',
description: 'Listar modelos disponiveis para um tipo de adaptador',
inputSchema: {
type: 'object',
properties: {
adapter_type: { type: 'string', description: 'Tipo de adaptador' },
},
required: ['adapter_type'],
},
handler: async (args) => {
const { adapter_type } = args as { adapter_type: string; [k: string]: unknown };
const result = await client.get(client.companyPath(`/adapters/${adapter_type}/models`));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'test_adapter_environment',
description: 'Testar o ambiente de um adaptador',
inputSchema: {
type: 'object',
properties: {
adapter_type: { type: 'string', description: 'Tipo de adaptador' },
},
required: ['adapter_type'],
},
handler: async (args) => {
const { adapter_type, ...body } = args as { adapter_type: string; [k: string]: unknown };
const result = await client.post(client.companyPath(`/adapters/${adapter_type}/test-environment`), body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+57
View File
@@ -0,0 +1,57 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const agentKeyTools: PaperclipTool[] = [
{
name: 'list_agent_keys',
description: 'Listar chaves de API de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/keys`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_agent_key',
description: 'Criar uma nova chave de API para um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
name: { type: 'string', description: 'Nome da chave' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/keys`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_agent_key',
description: 'Eliminar uma chave de API de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
key_id: { type: 'string', description: 'ID da chave' },
},
required: ['agent_id', 'key_id'],
},
handler: async (args) => {
const { agent_id, key_id } = args as { agent_id: string; key_id: string; [k: string]: unknown };
const result = await client.delete(`/agents/${agent_id}/keys/${key_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+378
View File
@@ -0,0 +1,378 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const agentTools: PaperclipTool[] = [
// READ (11)
{
name: 'list_agents',
description: 'Listar todos os agentes da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/agents'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent',
description: 'Obter detalhes de um agente pelo ID',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_runtime_state',
description: 'Obter estado de execucao atual de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/runtime-state`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_configuration',
description: 'Obter configuracao atual de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/configuration`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_config_revisions',
description: 'Listar revisoes de configuracao de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/config-revisions`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_skills',
description: 'Listar skills (capacidades) de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/skills`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_task_sessions',
description: 'Listar sessoes de tarefas de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/task-sessions`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_agent_instructions_bundle',
description: 'Obter bundle de instrucoes de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.get(`/agents/${agent_id}/instructions-bundle`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'rollback_agent_config',
description: 'Reverter configuracao de um agente para uma revisao anterior',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
revision_id: { type: 'string', description: 'ID da revisao para reverter' },
},
required: ['agent_id', 'revision_id'],
},
handler: async (args) => {
const { agent_id, revision_id, ...body } = args as { agent_id: string; revision_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/config-revisions/${revision_id}/rollback`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'sync_agent_skills',
description: 'Sincronizar skills de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/skills/sync`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_agent_instructions_bundle',
description: 'Actualizar bundle de instrucoes de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
instructions: { type: 'string', description: 'Conteudo das instrucoes' },
},
required: ['agent_id', 'instructions'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.patch(`/agents/${agent_id}/instructions-bundle`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
// WRITE (7)
{
name: 'create_agent',
description: 'Criar um novo agente na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome do agente' },
role: { type: 'string', description: 'Funcao do agente' },
title: { type: 'string', description: 'Titulo do agente' },
reports_to: { type: 'string', description: 'ID do agente superior hierarquico' },
capabilities: { type: 'array', items: { type: 'string' }, description: 'Lista de capacidades do agente' },
model: { type: 'string', description: 'Modelo de IA a utilizar' },
budget_monthly_cents: { type: 'number', description: 'Orcamento mensal em centimos' },
instructions_file_path: { type: 'string', description: 'Caminho do ficheiro de instrucoes' },
},
required: ['name', 'role'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/agents'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_agent_hire',
description: 'Criar uma contratacao de agente na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome do agente a contratar' },
role: { type: 'string', description: 'Funcao do agente a contratar' },
title: { type: 'string', description: 'Titulo do agente' },
reports_to: { type: 'string', description: 'ID do agente superior hierarquico' },
},
required: ['name', 'role'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/agent-hires'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_agent',
description: 'Actualizar dados de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
name: { type: 'string', description: 'Novo nome do agente' },
title: { type: 'string', description: 'Novo titulo do agente' },
model: { type: 'string', description: 'Modelo de IA a utilizar' },
budget_monthly_cents: { type: 'number', description: 'Orcamento mensal em centimos' },
capabilities: { type: 'array', items: { type: 'string' }, description: 'Lista de capacidades do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.patch(`/agents/${agent_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_agent_permissions',
description: 'Actualizar permissoes de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
permissions: { type: 'object', description: 'Objecto de permissoes a actualizar' },
},
required: ['agent_id', 'permissions'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.patch(`/agents/${agent_id}/permissions`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_agent_instructions_path',
description: 'Actualizar o caminho do ficheiro de instrucoes de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
instructions_file_path: { type: 'string', description: 'Novo caminho do ficheiro de instrucoes' },
},
required: ['agent_id', 'instructions_file_path'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.patch(`/agents/${agent_id}/instructions-path`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
// LIFECYCLE (4)
{
name: 'pause_agent',
description: 'Pausar um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/pause`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'resume_agent',
description: 'Retomar um agente pausado',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/resume`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'wakeup_agent',
description: 'Acordar um agente com uma mensagem ou issue opcional',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
issue_id: { type: 'string', description: 'ID da issue associada (opcional)' },
message: { type: 'string', description: 'Mensagem para o agente (opcional)' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/wakeup`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'invoke_agent_heartbeat',
description: 'Invocar o heartbeat de um agente manualmente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/heartbeat/invoke`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
// DESTRUCTIVE (2)
{
name: 'terminate_agent',
description: 'Terminar (desligar permanentemente) um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.post(`/agents/${agent_id}/terminate`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_agent',
description: 'Eliminar permanentemente um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id } = args as { agent_id: string; [k: string]: unknown };
const result = await client.delete(`/agents/${agent_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+167
View File
@@ -0,0 +1,167 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const approvalTools: PaperclipTool[] = [
{
name: 'list_approvals',
description: 'Listar todas as aprovacoes da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/approvals'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_approval',
description: 'Obter detalhes de uma aprovacao pelo ID',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id } = args as { approval_id: string; [k: string]: unknown };
const result = await client.get(`/approvals/${approval_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_approval',
description: 'Criar uma nova aprovacao na empresa',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Titulo da aprovacao' },
description: { type: 'string', description: 'Descricao da aprovacao' },
type: { type: 'string', description: 'Tipo de aprovacao' },
issue_id: { type: 'string', description: 'ID da issue associada' },
},
required: ['title'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/approvals'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_approval_issues',
description: 'Listar issues associadas a uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id } = args as { approval_id: string; [k: string]: unknown };
const result = await client.get(`/approvals/${approval_id}/issues`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'approve_approval',
description: 'Aprovar uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
comment: { type: 'string', description: 'Comentario de aprovacao (opcional)' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id, ...body } = args as { approval_id: string; [k: string]: unknown };
const result = await client.post(`/approvals/${approval_id}/approve`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'reject_approval',
description: 'Rejeitar uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
reason: { type: 'string', description: 'Motivo da rejeicao (opcional)' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id, ...body } = args as { approval_id: string; [k: string]: unknown };
const result = await client.post(`/approvals/${approval_id}/reject`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'request_approval_revision',
description: 'Solicitar revisao de uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
feedback: { type: 'string', description: 'Feedback para a revisao (opcional)' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id, ...body } = args as { approval_id: string; [k: string]: unknown };
const result = await client.post(`/approvals/${approval_id}/request-revision`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'resubmit_approval',
description: 'Resubmeter uma aprovacao apos revisao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
changes_description: { type: 'string', description: 'Descricao das alteracoes efectuadas (opcional)' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id, ...body } = args as { approval_id: string; [k: string]: unknown };
const result = await client.post(`/approvals/${approval_id}/resubmit`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_approval_comments',
description: 'Listar comentarios de uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
},
required: ['approval_id'],
},
handler: async (args) => {
const { approval_id } = args as { approval_id: string; [k: string]: unknown };
const result = await client.get(`/approvals/${approval_id}/comments`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'add_approval_comment',
description: 'Adicionar um comentario a uma aprovacao',
inputSchema: {
type: 'object',
properties: {
approval_id: { type: 'string', description: 'ID da aprovacao' },
body: { type: 'string', description: 'Conteudo do comentario' },
},
required: ['approval_id', 'body'],
},
handler: async (args) => {
const { approval_id, ...body } = args as { approval_id: string; [k: string]: unknown };
const result = await client.post(`/approvals/${approval_id}/comments`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+55
View File
@@ -0,0 +1,55 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const assetTools: PaperclipTool[] = [
{
name: 'upload_image_asset',
description: 'Carregar uma imagem como asset da empresa',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Nome do ficheiro' },
content: { type: 'string', description: 'Conteúdo do ficheiro em base64' },
},
required: ['filename', 'content'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/assets/images'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'upload_company_logo',
description: 'Carregar o logótipo da empresa',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Nome do ficheiro' },
content: { type: 'string', description: 'Conteúdo do ficheiro em base64' },
},
required: ['filename', 'content'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/logo'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_asset_content',
description: 'Obter conteúdo de um asset pelo ID',
inputSchema: {
type: 'object',
properties: {
asset_id: { type: 'string', description: 'ID do asset' },
},
required: ['asset_id'],
},
handler: async (args) => {
const { asset_id } = args as { asset_id: string };
const result = await client.get(`/assets/${asset_id}/content`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+73
View File
@@ -0,0 +1,73 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const attachmentTools: PaperclipTool[] = [
{
name: 'list_issue_attachments',
description: 'Listar anexos de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/attachments`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'upload_issue_attachment',
description: 'Carregar um anexo para uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
filename: { type: 'string', description: 'Nome do ficheiro' },
content: { type: 'string', description: 'Conteudo do ficheiro em base64' },
},
required: ['issue_id', 'filename', 'content'],
},
handler: async (args) => {
const { issue_id, ...body } = args as { issue_id: string; [k: string]: unknown };
const result = await client.post(client.companyPath(`/issues/${issue_id}/attachments`), body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_attachment_content',
description: 'Obter o conteudo de um anexo',
inputSchema: {
type: 'object',
properties: {
attachment_id: { type: 'string', description: 'ID do anexo' },
},
required: ['attachment_id'],
},
handler: async (args) => {
const { attachment_id } = args as { attachment_id: string; [k: string]: unknown };
const result = await client.get(`/attachments/${attachment_id}/content`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_attachment',
description: 'Eliminar um anexo',
inputSchema: {
type: 'object',
properties: {
attachment_id: { type: 'string', description: 'ID do anexo' },
},
required: ['attachment_id'],
},
handler: async (args) => {
const { attachment_id } = args as { attachment_id: string; [k: string]: unknown };
const result = await client.delete(`/attachments/${attachment_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+117
View File
@@ -0,0 +1,117 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const companyTools: PaperclipTool[] = [
{
name: 'get_company',
description: 'Obter detalhes da empresa Paperclip',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath(''));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_companies',
description: 'Listar todas as empresas na instancia Paperclip',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/companies');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_company_stats',
description: 'Estatisticas agregadas da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/companies/stats');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_company',
description: 'Actualizar dados da empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome da empresa' },
description: { type: 'string', description: 'Descricao da empresa' },
},
},
handler: async (args) => {
const result = await client.patch(client.companyPath(''), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_company_branding',
description: 'Actualizar branding da empresa',
inputSchema: {
type: 'object',
properties: {
primary_color: { type: 'string', description: 'Cor primaria (hex)' },
accent_color: { type: 'string', description: 'Cor de destaque (hex)' },
},
},
handler: async (args) => {
const result = await client.patch(client.companyPath('/branding'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_org_chart',
description: 'Obter organigrama — hierarquia JSON de todos os agentes',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/org'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_dashboard',
description: 'Dashboard agregado — metricas de agentes, issues e custos',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/dashboard'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_members',
description: 'Listar membros (utilizadores humanos) da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/members'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_member_permissions',
description: 'Actualizar permissoes de um membro',
inputSchema: {
type: 'object',
properties: {
member_id: { type: 'string', description: 'ID do membro' },
permissions: { type: 'object', description: 'Objecto de permissoes a actualizar' },
},
required: ['member_id', 'permissions'],
},
handler: async (args) => {
const { member_id, ...body } = args as { member_id: string; [k: string]: unknown };
const result = await client.patch(client.companyPath(`/members/${member_id}/permissions`), body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_sidebar_badges',
description: 'Obter badges do sidebar (contadores pendentes)',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/sidebar-badges'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+128
View File
@@ -0,0 +1,128 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const costTools: PaperclipTool[] = [
{
name: 'get_cost_summary',
description: 'Obter resumo de custos da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/summary'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_costs_by_agent',
description: 'Obter custos agrupados por agente',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/by-agent'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_costs_by_agent_model',
description: 'Obter custos agrupados por agente e modelo',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/by-agent-model'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_costs_by_provider',
description: 'Obter custos agrupados por fornecedor',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/by-provider'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_costs_by_project',
description: 'Obter custos agrupados por projecto',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/by-project'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_finance_summary',
description: 'Obter resumo financeiro da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/finance-summary'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_finance_events',
description: 'Listar eventos financeiros da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/finance-events'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_window_spend',
description: 'Obter gasto na janela temporal actual',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/window-spend'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_quota_windows',
description: 'Obter janelas de quota da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/costs/quota-windows'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_budgets_overview',
description: 'Obter visao geral dos orcamentos da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/budgets/overview'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_company_budget',
description: 'Actualizar orcamento mensal da empresa',
inputSchema: {
type: 'object',
properties: {
monthly_budget_cents: { type: 'number', description: 'Orcamento mensal em centimos' },
},
},
handler: async (args) => {
const result = await client.patch(client.companyPath('/budgets'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_agent_budget',
description: 'Actualizar orcamento mensal de um agente',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'ID do agente' },
monthly_budget_cents: { type: 'number', description: 'Orcamento mensal em centimos' },
},
required: ['agent_id'],
},
handler: async (args) => {
const { agent_id, ...body } = args as { agent_id: string; [k: string]: unknown };
const result = await client.patch(`/agents/${agent_id}/budgets`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+50
View File
@@ -0,0 +1,50 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const executionWorkspaceTools: PaperclipTool[] = [
{
name: 'list_execution_workspaces',
description: 'Listar todos os espacos de execucao da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/execution-workspaces'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_execution_workspace',
description: 'Obter detalhes de um espaco de execucao pelo ID',
inputSchema: {
type: 'object',
properties: {
workspace_id: { type: 'string', description: 'ID do espaco de execucao' },
},
required: ['workspace_id'],
},
handler: async (args) => {
const { workspace_id } = args as { workspace_id: string; [k: string]: unknown };
const result = await client.get(`/execution-workspaces/${workspace_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_execution_workspace',
description: 'Actualizar um espaco de execucao',
inputSchema: {
type: 'object',
properties: {
workspace_id: { type: 'string', description: 'ID do espaco de execucao' },
name: { type: 'string', description: 'Novo nome do espaco de execucao' },
config: { type: 'object', description: 'Configuracao do espaco de execucao' },
},
required: ['workspace_id'],
},
handler: async (args) => {
const { workspace_id, ...body } = args as { workspace_id: string; [k: string]: unknown };
const result = await client.patch(`/execution-workspaces/${workspace_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+87
View File
@@ -0,0 +1,87 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const goalTools: PaperclipTool[] = [
{
name: 'list_goals',
description: 'Listar todos os objectivos da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/goals'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_goal',
description: 'Obter detalhes de um objectivo pelo ID',
inputSchema: {
type: 'object',
properties: {
goal_id: { type: 'string', description: 'ID do objectivo' },
},
required: ['goal_id'],
},
handler: async (args) => {
const { goal_id } = args as { goal_id: string; [k: string]: unknown };
const result = await client.get(`/goals/${goal_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_goal',
description: 'Criar um novo objectivo na empresa',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Titulo do objectivo' },
description: { type: 'string', description: 'Descricao do objectivo' },
target_date: { type: 'string', description: 'Data alvo do objectivo' },
assignee_id: { type: 'string', description: 'ID do responsavel' },
priority: { type: 'string', description: 'Prioridade do objectivo' },
},
required: ['title'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/goals'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_goal',
description: 'Actualizar um objectivo existente',
inputSchema: {
type: 'object',
properties: {
goal_id: { type: 'string', description: 'ID do objectivo' },
title: { type: 'string', description: 'Novo titulo do objectivo' },
description: { type: 'string', description: 'Nova descricao do objectivo' },
status: { type: 'string', description: 'Estado do objectivo' },
progress: { type: 'number', description: 'Progresso do objectivo (0-100)' },
},
required: ['goal_id'],
},
handler: async (args) => {
const { goal_id, ...body } = args as { goal_id: string; [k: string]: unknown };
const result = await client.patch(`/goals/${goal_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_goal',
description: 'Eliminar um objectivo pelo ID',
inputSchema: {
type: 'object',
properties: {
goal_id: { type: 'string', description: 'ID do objectivo' },
},
required: ['goal_id'],
},
handler: async (args) => {
const { goal_id } = args as { goal_id: string; [k: string]: unknown };
const result = await client.delete(`/goals/${goal_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+16
View File
@@ -0,0 +1,16 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const healthTools: PaperclipTool[] = [
{
name: 'get_health',
description: 'Verificar estado do Paperclip — retorna versao, modo e bootstrap status',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/health');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+89
View File
@@ -0,0 +1,89 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const heartbeatRunTools: PaperclipTool[] = [
{
name: 'list_heartbeat_runs',
description: 'Listar todas as execucoes de heartbeat da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/heartbeat-runs'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_live_runs',
description: 'Listar execucoes em curso (live runs) da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/live-runs'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_heartbeat_run',
description: 'Obter detalhes de uma execucao de heartbeat pelo ID',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'string', description: 'ID da execucao de heartbeat' },
},
required: ['run_id'],
},
handler: async (args) => {
const { run_id } = args as { run_id: string; [k: string]: unknown };
const result = await client.get(`/heartbeat-runs/${run_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_heartbeat_run_events',
description: 'Obter eventos de uma execucao de heartbeat',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'string', description: 'ID da execucao de heartbeat' },
},
required: ['run_id'],
},
handler: async (args) => {
const { run_id } = args as { run_id: string; [k: string]: unknown };
const result = await client.get(`/heartbeat-runs/${run_id}/events`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_heartbeat_run_log',
description: 'Obter log de uma execucao de heartbeat',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'string', description: 'ID da execucao de heartbeat' },
},
required: ['run_id'],
},
handler: async (args) => {
const { run_id } = args as { run_id: string; [k: string]: unknown };
const result = await client.get(`/heartbeat-runs/${run_id}/log`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'cancel_heartbeat_run',
description: 'Cancelar uma execucao de heartbeat em curso',
inputSchema: {
type: 'object',
properties: {
run_id: { type: 'string', description: 'ID da execucao de heartbeat' },
},
required: ['run_id'],
},
handler: async (args) => {
const { run_id, ...body } = args as { run_id: string; [k: string]: unknown };
const result = await client.post(`/heartbeat-runs/${run_id}/cancel`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+52
View File
@@ -0,0 +1,52 @@
import { PaperclipTool } from '../types.js';
import { healthTools } from './health.js';
import { companyTools } from './company.js';
import { agentTools } from './agents.js';
import { agentKeyTools } from './agent-keys.js';
import { heartbeatRunTools } from './heartbeat-runs.js';
import { issueTools } from './issues.js';
import { labelTools } from './labels.js';
import { attachmentTools } from './attachments.js';
import { approvalTools } from './approvals.js';
import { routineTools } from './routines.js';
import { goalTools } from './goals.js';
import { projectTools } from './projects.js';
import { costTools } from './costs.js';
import { activityTools } from './activity.js';
import { skillTools } from './skills.js';
import { secretTools } from './secrets.js';
import { executionWorkspaceTools } from './execution-workspaces.js';
import { adapterTools } from './adapters.js';
import { portabilityTools } from './portability.js';
import { pluginTools } from './plugins.js';
import { pluginBridgeTools } from './plugin-bridge.js';
import { assetTools } from './assets.js';
import { settingsTools } from './settings.js';
import { accessTools } from './access.js';
export const allTools: PaperclipTool[] = [
...healthTools,
...companyTools,
...agentTools,
...agentKeyTools,
...heartbeatRunTools,
...issueTools,
...labelTools,
...attachmentTools,
...approvalTools,
...routineTools,
...goalTools,
...projectTools,
...costTools,
...activityTools,
...skillTools,
...secretTools,
...executionWorkspaceTools,
...adapterTools,
...portabilityTools,
...pluginTools,
...pluginBridgeTools,
...assetTools,
...settingsTools,
...accessTools,
];
+304
View File
@@ -0,0 +1,304 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const issueTools: PaperclipTool[] = [
{
name: 'list_issues',
description: 'Listar todas as issues da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/issues'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_issue',
description: 'Obter detalhes de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_issue_heartbeat_context',
description: 'Obter contexto de heartbeat de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/heartbeat-context`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_issue',
description: 'Criar uma nova issue',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Titulo da issue' },
description: { type: 'string', description: 'Descricao da issue' },
assignee_id: { type: 'string', description: 'ID do responsavel' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'urgent'],
description: 'Prioridade da issue',
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Lista de etiquetas',
},
},
required: ['title'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/issues'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_issue',
description: 'Actualizar uma issue existente',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
title: { type: 'string', description: 'Titulo da issue' },
description: { type: 'string', description: 'Descricao da issue' },
assignee_id: { type: 'string', description: 'ID do responsavel' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'urgent'],
description: 'Prioridade da issue',
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Lista de etiquetas',
},
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id, ...body } = args as { issue_id: string; [k: string]: unknown };
const result = await client.patch(`/issues/${issue_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_issue',
description: 'Eliminar uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.delete(`/issues/${issue_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'checkout_issue',
description: 'Fazer checkout de uma issue (atribuir ao agente actual)',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.post(`/issues/${issue_id}/checkout`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'release_issue',
description: 'Libertar uma issue (remover agente actual)',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.post(`/issues/${issue_id}/release`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_issue_comments',
description: 'Listar comentarios de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/comments`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'add_issue_comment',
description: 'Adicionar um comentario a uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
body: { type: 'string', description: 'Conteudo do comentario' },
},
required: ['issue_id', 'body'],
},
handler: async (args) => {
const { issue_id, ...body } = args as { issue_id: string; [k: string]: unknown };
const result = await client.post(`/issues/${issue_id}/comments`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_issue_documents',
description: 'Listar documentos associados a uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/documents`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_issue_document',
description: 'Obter um documento especifico de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
key: { type: 'string', description: 'Chave do documento' },
},
required: ['issue_id', 'key'],
},
handler: async (args) => {
const { issue_id, key } = args as { issue_id: string; key: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/documents/${key}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'upsert_issue_document',
description: 'Criar ou actualizar um documento de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
key: { type: 'string', description: 'Chave do documento' },
content: { type: 'string', description: 'Conteudo do documento' },
},
required: ['issue_id', 'key', 'content'],
},
handler: async (args) => {
const { issue_id, key, ...body } = args as { issue_id: string; key: string; [k: string]: unknown };
const result = await client.put(`/issues/${issue_id}/documents/${key}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_issue_document',
description: 'Eliminar um documento de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
key: { type: 'string', description: 'Chave do documento' },
},
required: ['issue_id', 'key'],
},
handler: async (args) => {
const { issue_id, key } = args as { issue_id: string; key: string; [k: string]: unknown };
const result = await client.delete(`/issues/${issue_id}/documents/${key}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_issue_work_products',
description: 'Listar produtos de trabalho de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/work-products`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_issue_work_product',
description: 'Criar um produto de trabalho para uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
title: { type: 'string', description: 'Titulo do produto de trabalho' },
description: { type: 'string', description: 'Descricao do produto de trabalho' },
url: { type: 'string', description: 'URL do produto de trabalho' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id, ...body } = args as { issue_id: string; [k: string]: unknown };
const result = await client.post(`/issues/${issue_id}/work-products`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_issue_live_runs',
description: 'Listar execucoes em curso de uma issue',
inputSchema: {
type: 'object',
properties: {
issue_id: { type: 'string', description: 'ID da issue' },
},
required: ['issue_id'],
},
handler: async (args) => {
const { issue_id } = args as { issue_id: string; [k: string]: unknown };
const result = await client.get(`/issues/${issue_id}/live-runs`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+48
View File
@@ -0,0 +1,48 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const labelTools: PaperclipTool[] = [
{
name: 'list_labels',
description: 'Listar todas as etiquetas da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/labels'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_label',
description: 'Criar uma nova etiqueta',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome da etiqueta' },
color: { type: 'string', description: 'Cor da etiqueta (hex)' },
},
required: ['name'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/labels'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_label',
description: 'Eliminar uma etiqueta',
inputSchema: {
type: 'object',
properties: {
label_id: { type: 'string', description: 'ID da etiqueta' },
},
required: ['label_id'],
},
handler: async (args) => {
const { label_id } = args as { label_id: string; [k: string]: unknown };
const result = await client.delete(`/labels/${label_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+67
View File
@@ -0,0 +1,67 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const pluginBridgeTools: PaperclipTool[] = [
{
name: 'execute_plugin_tool',
description: 'Executar uma ferramenta de plugin pelo nome',
inputSchema: {
type: 'object',
properties: {
tool_name: { type: 'string', description: 'Nome da ferramenta a executar' },
arguments: { type: 'object', description: 'Argumentos para a ferramenta (opcional)' },
},
required: ['tool_name'],
},
handler: async (args) => {
const result = await client.post('/plugins/tools/execute', args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'plugin_bridge_data',
description: 'Obter dados através da ponte de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
query: { type: 'object', description: 'Consulta de dados (opcional)' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id, ...body } = args as { plugin_id: string; [k: string]: unknown };
const result = await client.post(`/plugins/${plugin_id}/bridge/data`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'plugin_bridge_action',
description: 'Executar uma acção através da ponte de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
action: { type: 'string', description: 'Nome da acção a executar' },
params: { type: 'object', description: 'Parâmetros da acção (opcional)' },
},
required: ['plugin_id', 'action'],
},
handler: async (args) => {
const { plugin_id, ...body } = args as { plugin_id: string; [k: string]: unknown };
const result = await client.post(`/plugins/${plugin_id}/bridge/action`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_ui_contributions',
description: 'Listar contribuições de interface dos plugins',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/plugins/ui-contributions');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+262
View File
@@ -0,0 +1,262 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const pluginTools: PaperclipTool[] = [
{
name: 'list_plugins',
description: 'Listar todos os plugins disponíveis',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/plugins');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_plugin',
description: 'Obter detalhes de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_plugin_examples',
description: 'Listar exemplos de plugins disponíveis',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/plugins/examples');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_plugin_tools',
description: 'Listar ferramentas disponíveis nos plugins',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/plugins/tools');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'install_plugin',
description: 'Instalar um plugin a partir de uma fonte',
inputSchema: {
type: 'object',
properties: {
source: { type: 'string', description: 'Fonte do plugin' },
url: { type: 'string', description: 'URL do plugin (opcional)' },
config: { type: 'object', description: 'Configuração do plugin (opcional)' },
},
required: ['source'],
},
handler: async (args) => {
const result = await client.post('/plugins/install', args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_plugin',
description: 'Eliminar um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.delete(`/plugins/${plugin_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'enable_plugin',
description: 'Activar um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.post(`/plugins/${plugin_id}/enable`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'disable_plugin',
description: 'Desactivar um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.post(`/plugins/${plugin_id}/disable`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'upgrade_plugin',
description: 'Actualizar um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.post(`/plugins/${plugin_id}/upgrade`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_plugin_health',
description: 'Obter estado de saúde de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}/health`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_plugin_logs',
description: 'Obter registos de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}/logs`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_plugin_config',
description: 'Obter configuração de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}/config`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'set_plugin_config',
description: 'Definir configuração de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
config: { type: 'object', description: 'Configuração do plugin' },
},
required: ['plugin_id', 'config'],
},
handler: async (args) => {
const { plugin_id, config } = args as { plugin_id: string; config: object };
const result = await client.post(`/plugins/${plugin_id}/config`, config);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'test_plugin_config',
description: 'Testar configuração de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
config: { type: 'object', description: 'Configuração a testar' },
},
required: ['plugin_id', 'config'],
},
handler: async (args) => {
const { plugin_id, config } = args as { plugin_id: string; config: object };
const result = await client.post(`/plugins/${plugin_id}/config/test`, config);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_plugin_dashboard',
description: 'Obter painel de controlo de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}/dashboard`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_plugin_jobs',
description: 'Listar tarefas agendadas de um plugin pelo ID',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
},
required: ['plugin_id'],
},
handler: async (args) => {
const { plugin_id } = args as { plugin_id: string };
const result = await client.get(`/plugins/${plugin_id}/jobs`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'trigger_plugin_job',
description: 'Disparar manualmente uma tarefa de um plugin',
inputSchema: {
type: 'object',
properties: {
plugin_id: { type: 'string', description: 'ID do plugin' },
job_id: { type: 'string', description: 'ID da tarefa' },
},
required: ['plugin_id', 'job_id'],
},
handler: async (args) => {
const { plugin_id, job_id } = args as { plugin_id: string; job_id: string };
const result = await client.post(`/plugins/${plugin_id}/jobs/${job_id}/trigger`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+31
View File
@@ -0,0 +1,31 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const portabilityTools: PaperclipTool[] = [
{
name: 'export_company',
description: 'Exportar dados da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async (args) => {
const result = await client.post(client.companyPath('/exports'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'import_company_preview',
description: 'Pre-visualizar importacao de dados para a empresa',
inputSchema: {
type: 'object',
properties: {
data: { type: 'object', description: 'Dados a importar' },
},
required: ['data'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/imports/preview'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+118
View File
@@ -0,0 +1,118 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const projectTools: PaperclipTool[] = [
{
name: 'list_projects',
description: 'Listar todos os projectos da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/projects'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_project',
description: 'Obter detalhes de um projecto pelo ID',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'ID do projecto' },
},
required: ['project_id'],
},
handler: async (args) => {
const { project_id } = args as { project_id: string; [k: string]: unknown };
const result = await client.get(`/projects/${project_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_project',
description: 'Criar um novo projecto na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome do projecto' },
description: { type: 'string', description: 'Descricao do projecto' },
repository_url: { type: 'string', description: 'URL do repositorio associado' },
},
required: ['name'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/projects'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_project',
description: 'Actualizar um projecto existente',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'ID do projecto' },
name: { type: 'string', description: 'Novo nome do projecto' },
description: { type: 'string', description: 'Nova descricao do projecto' },
status: { type: 'string', description: 'Estado do projecto' },
},
required: ['project_id'],
},
handler: async (args) => {
const { project_id, ...body } = args as { project_id: string; [k: string]: unknown };
const result = await client.patch(`/projects/${project_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_project',
description: 'Eliminar um projecto pelo ID',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'ID do projecto' },
},
required: ['project_id'],
},
handler: async (args) => {
const { project_id } = args as { project_id: string; [k: string]: unknown };
const result = await client.delete(`/projects/${project_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_project_workspaces',
description: 'Listar workspaces de um projecto',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'ID do projecto' },
},
required: ['project_id'],
},
handler: async (args) => {
const { project_id } = args as { project_id: string; [k: string]: unknown };
const result = await client.get(`/projects/${project_id}/workspaces`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_project_workspace',
description: 'Criar um workspace num projecto',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string', description: 'ID do projecto' },
name: { type: 'string', description: 'Nome do workspace' },
path: { type: 'string', description: 'Caminho do workspace' },
},
required: ['project_id'],
},
handler: async (args) => {
const { project_id, ...body } = args as { project_id: string; [k: string]: unknown };
const result = await client.post(`/projects/${project_id}/workspaces`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+136
View File
@@ -0,0 +1,136 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const routineTools: PaperclipTool[] = [
{
name: 'list_routines',
description: 'Listar todas as rotinas da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/routines'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_routine',
description: 'Obter detalhes de uma rotina pelo ID',
inputSchema: {
type: 'object',
properties: {
routine_id: { type: 'string', description: 'ID da rotina' },
},
required: ['routine_id'],
},
handler: async (args) => {
const { routine_id } = args as { routine_id: string; [k: string]: unknown };
const result = await client.get(`/routines/${routine_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_routine',
description: 'Criar uma nova rotina na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome da rotina' },
description: { type: 'string', description: 'Descricao da rotina' },
agent_id: { type: 'string', description: 'ID do agente associado' },
schedule: { type: 'string', description: 'Agendamento da rotina (ex: expressao cron)' },
},
required: ['name'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/routines'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_routine',
description: 'Actualizar uma rotina existente',
inputSchema: {
type: 'object',
properties: {
routine_id: { type: 'string', description: 'ID da rotina' },
name: { type: 'string', description: 'Novo nome da rotina' },
description: { type: 'string', description: 'Nova descricao da rotina' },
schedule: { type: 'string', description: 'Novo agendamento da rotina' },
enabled: { type: 'boolean', description: 'Estado de activacao da rotina' },
},
required: ['routine_id'],
},
handler: async (args) => {
const { routine_id, ...body } = args as { routine_id: string; [k: string]: unknown };
const result = await client.patch(`/routines/${routine_id}`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_routine_runs',
description: 'Listar execucoes de uma rotina',
inputSchema: {
type: 'object',
properties: {
routine_id: { type: 'string', description: 'ID da rotina' },
},
required: ['routine_id'],
},
handler: async (args) => {
const { routine_id } = args as { routine_id: string; [k: string]: unknown };
const result = await client.get(`/routines/${routine_id}/runs`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'run_routine',
description: 'Executar uma rotina manualmente',
inputSchema: {
type: 'object',
properties: {
routine_id: { type: 'string', description: 'ID da rotina' },
},
required: ['routine_id'],
},
handler: async (args) => {
const { routine_id, ...body } = args as { routine_id: string; [k: string]: unknown };
const result = await client.post(`/routines/${routine_id}/run`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_routine_trigger',
description: 'Criar um gatilho para uma rotina',
inputSchema: {
type: 'object',
properties: {
routine_id: { type: 'string', description: 'ID da rotina' },
type: { type: 'string', description: 'Tipo de gatilho' },
config: { type: 'object', description: 'Configuracao do gatilho' },
},
required: ['routine_id'],
},
handler: async (args) => {
const { routine_id, ...body } = args as { routine_id: string; [k: string]: unknown };
const result = await client.post(`/routines/${routine_id}/triggers`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_routine_trigger',
description: 'Eliminar um gatilho de rotina pelo ID',
inputSchema: {
type: 'object',
properties: {
trigger_id: { type: 'string', description: 'ID do gatilho' },
},
required: ['trigger_id'],
},
handler: async (args) => {
const { trigger_id } = args as { trigger_id: string; [k: string]: unknown };
const result = await client.delete(`/routine-triggers/${trigger_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+74
View File
@@ -0,0 +1,74 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const secretTools: PaperclipTool[] = [
{
name: 'list_secrets',
description: 'Listar todos os segredos da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/secrets'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_secret_providers',
description: 'Listar fornecedores de segredos disponiveis',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/secret-providers'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_secret',
description: 'Criar um novo segredo na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome do segredo' },
value: { type: 'string', description: 'Valor do segredo' },
provider: { type: 'string', description: 'Fornecedor do segredo' },
},
required: ['name', 'value'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/secrets'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'rotate_secret',
description: 'Rodar (renovar) um segredo pelo ID',
inputSchema: {
type: 'object',
properties: {
secret_id: { type: 'string', description: 'ID do segredo' },
},
required: ['secret_id'],
},
handler: async (args) => {
const { secret_id, ...body } = args as { secret_id: string; [k: string]: unknown };
const result = await client.post(`/secrets/${secret_id}/rotate`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_secret',
description: 'Eliminar um segredo pelo ID',
inputSchema: {
type: 'object',
properties: {
secret_id: { type: 'string', description: 'ID do segredo' },
},
required: ['secret_id'],
},
handler: async (args) => {
const { secret_id } = args as { secret_id: string; [k: string]: unknown };
const result = await client.delete(`/secrets/${secret_id}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+53
View File
@@ -0,0 +1,53 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const settingsTools: PaperclipTool[] = [
{
name: 'get_general_settings',
description: 'Obter definições gerais da instância',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/instance/settings/general');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_general_settings',
description: 'Actualizar definições gerais da instância',
inputSchema: {
type: 'object',
properties: {
settings: { type: 'object', description: 'Definições a actualizar (opcional)' },
},
},
handler: async (args) => {
const result = await client.patch('/instance/settings/general', args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_experimental_settings',
description: 'Obter definições experimentais da instância',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get('/instance/settings/experimental');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'update_experimental_settings',
description: 'Actualizar definições experimentais da instância',
inputSchema: {
type: 'object',
properties: {
settings: { type: 'object', description: 'Definições experimentais a actualizar (opcional)' },
},
},
handler: async (args) => {
const result = await client.patch('/instance/settings/experimental', args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+97
View File
@@ -0,0 +1,97 @@
import { PaperclipClient } from '../client.js';
import { PaperclipTool } from '../types.js';
const client = new PaperclipClient();
export const skillTools: PaperclipTool[] = [
{
name: 'list_company_skills',
description: 'Listar todas as skills da empresa',
inputSchema: { type: 'object', properties: {} },
handler: async () => {
const result = await client.get(client.companyPath('/skills'));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'get_company_skill',
description: 'Obter detalhes de uma skill da empresa pelo ID',
inputSchema: {
type: 'object',
properties: {
skill_id: { type: 'string', description: 'ID da skill' },
},
required: ['skill_id'],
},
handler: async (args) => {
const { skill_id } = args as { skill_id: string; [k: string]: unknown };
const result = await client.get(client.companyPath(`/skills/${skill_id}`));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'list_skill_files',
description: 'Listar ficheiros de uma skill da empresa',
inputSchema: {
type: 'object',
properties: {
skill_id: { type: 'string', description: 'ID da skill' },
},
required: ['skill_id'],
},
handler: async (args) => {
const { skill_id } = args as { skill_id: string; [k: string]: unknown };
const result = await client.get(client.companyPath(`/skills/${skill_id}/files`));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'create_company_skill',
description: 'Criar uma nova skill na empresa',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Nome da skill' },
description: { type: 'string', description: 'Descricao da skill' },
type: { type: 'string', description: 'Tipo da skill' },
},
required: ['name'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/skills'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'import_company_skill',
description: 'Importar uma skill para a empresa a partir de uma fonte externa',
inputSchema: {
type: 'object',
properties: {
source: { type: 'string', description: 'Fonte de importacao da skill' },
url: { type: 'string', description: 'URL da skill a importar' },
},
required: ['source'],
},
handler: async (args) => {
const result = await client.post(client.companyPath('/skills/import'), args);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
{
name: 'delete_company_skill',
description: 'Eliminar uma skill da empresa',
inputSchema: {
type: 'object',
properties: {
skill_id: { type: 'string', description: 'ID da skill' },
},
required: ['skill_id'],
},
handler: async (args) => {
const { skill_id } = args as { skill_id: string; [k: string]: unknown };
const result = await client.delete(client.companyPath(`/skills/${skill_id}`));
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
},
},
];
+18
View File
@@ -0,0 +1,18 @@
export interface ToolResponse {
content: Array<{
type: 'text';
text: string;
}>;
[key: string]: unknown;
}
export interface PaperclipTool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
handler: (args: Record<string, unknown>) => Promise<ToolResponse>;
}
+34
View File
@@ -0,0 +1,34 @@
export function inferAnnotations(toolName: string): {
readOnlyHint: boolean;
destructiveHint: boolean;
idempotentHint: boolean;
openWorldHint: boolean;
} {
const name = toolName.toLowerCase();
const isReadOnly =
name.startsWith('get_') ||
name.startsWith('list_') ||
name.startsWith('search_') ||
name.endsWith('_summary') ||
name.endsWith('_overview');
const isDestructive =
name.startsWith('delete_') ||
name.startsWith('terminate_') ||
name.startsWith('revoke_') ||
name.startsWith('cancel_');
const isIdempotent =
isReadOnly ||
name.startsWith('update_') ||
name.startsWith('set_') ||
name.startsWith('upsert_');
return {
readOnlyHint: isReadOnly,
destructiveHint: isDestructive,
idempotentHint: isIdempotent,
openWorldHint: false,
};
}
+28
View File
@@ -0,0 +1,28 @@
import * as winston from 'winston';
const logLevel = process.env.LOG_LEVEL || 'error';
const logFile = process.env.LOG_FILE || 'logs/mcp-paperclip.log';
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json(),
);
const transports: winston.transport[] = [
new winston.transports.Console({
stderrLevels: ['error', 'warn', 'info', 'debug'],
format: winston.format.json(),
}),
];
if (logFile) {
transports.push(new winston.transports.File({ filename: logFile, format }));
}
export const logger = winston.createLogger({
level: logLevel,
format,
transports,
exitOnError: false,
});
+54
View File
@@ -0,0 +1,54 @@
import { PaperclipClient } from '../src/client.js';
const mockFetch = jest.fn();
global.fetch = mockFetch as any;
describe('PaperclipClient', () => {
let client: PaperclipClient;
beforeEach(() => {
process.env.PAPERCLIP_API_URL = 'https://clip.descomplicar.pt/api';
process.env.PAPERCLIP_API_KEY = 'test_key';
process.env.PAPERCLIP_COMPANY_ID = 'test-company-id';
client = new PaperclipClient();
mockFetch.mockReset();
});
test('GET sends correct headers', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 'ok' }) });
await client.get('/health');
expect(mockFetch).toHaveBeenCalledWith(
'https://clip.descomplicar.pt/api/health',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({ Authorization: 'Bearer test_key' }),
}),
);
});
test('companyPath injects company ID', () => {
expect(client.companyPath('/agents')).toBe('/companies/test-company-id/agents');
});
test('POST sends JSON body', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ id: '123' }) });
await client.post('/companies/test-company-id/issues', { title: 'Test' });
expect(mockFetch).toHaveBeenCalledWith(
'https://clip.descomplicar.pt/api/companies/test-company-id/issues',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ title: 'Test' }),
}),
);
});
test('handles 401 with clear error', async () => {
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', json: async () => ({ error: 'Unauthorized' }) });
await expect(client.get('/health')).rejects.toThrow('Sem autorização. Verificar PAPERCLIP_API_KEY.');
});
test('handles 404 with resource info', async () => {
mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found', json: async () => ({ error: 'Not found' }) });
await expect(client.get('/agents/abc')).rejects.toThrow('Recurso não encontrado: /agents/abc');
});
});
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"types": ["node", "jest"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}