easy-skill-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easy_skill_server-0.1.0.dist-info/METADATA +325 -0
- easy_skill_server-0.1.0.dist-info/RECORD +11 -0
- easy_skill_server-0.1.0.dist-info/WHEEL +5 -0
- easy_skill_server-0.1.0.dist-info/licenses/LICENSE +21 -0
- easy_skill_server-0.1.0.dist-info/top_level.txt +1 -0
- skillserver/__init__.py +30 -0
- skillserver/discovery.py +261 -0
- skillserver/models.py +71 -0
- skillserver/server.py +211 -0
- skillserver/tools.py +187 -0
- skillserver/utils.py +108 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy-skill-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Biblioteca para servir Agent Skills via Model Context Protocol (MCP)
|
|
5
|
+
Author-email: Serpro <vital@serpro.gov.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://git.serpro/vital/easy-skill-server
|
|
8
|
+
Project-URL: Documentation, https://git.serpro/vital/easy-skill-server/docs
|
|
9
|
+
Project-URL: Repository, https://git.serpro/vital/easy-skill-server
|
|
10
|
+
Keywords: mcp,agent,skills,ai,anthropic
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastmcp>=0.4.0
|
|
22
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# SkillServer MCP
|
|
35
|
+
|
|
36
|
+
**Biblioteca Python para servir Agent Skills via Model Context Protocol (MCP)**
|
|
37
|
+
|
|
38
|
+
SkillServer MCP é uma biblioteca que permite a qualquer pessoa criar e servir [Agent Skills](https://agentskills.io) facilmente através do [Model Context Protocol](https://modelcontextprotocol.io) da Anthropic.
|
|
39
|
+
|
|
40
|
+
## 🎯 O que é?
|
|
41
|
+
|
|
42
|
+
Uma biblioteca Python que:
|
|
43
|
+
- ✅ Descobre automaticamente skills de um diretório
|
|
44
|
+
- ✅ Expõe skills via MCP (HTTP/SSE)
|
|
45
|
+
- ✅ Fornece API simples com `create_server()`
|
|
46
|
+
- ✅ Inclui imagem Docker pronta para uso
|
|
47
|
+
- ✅ Segue princípios SOLID e boas práticas
|
|
48
|
+
|
|
49
|
+
## 🚀 Quick Start
|
|
50
|
+
|
|
51
|
+
### Instalação
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install easy-skill-server
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Uso via Python
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from skillserver import create_server
|
|
61
|
+
|
|
62
|
+
# Cria e inicia servidor de skills
|
|
63
|
+
create_server(skills_dir="./skills")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pronto! Seu servidor MCP está rodando em `http://localhost:8000`
|
|
67
|
+
|
|
68
|
+
### Uso via Docker
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Executar com Docker
|
|
72
|
+
docker run -p 8000:8000 \
|
|
73
|
+
-v ./skills:/skills:ro \
|
|
74
|
+
-e SKILLS_DIR=/skills \
|
|
75
|
+
easy-skill-server:latest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Ou use `docker-compose.yml`:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
version: '3.8'
|
|
82
|
+
|
|
83
|
+
services:
|
|
84
|
+
skillserver:
|
|
85
|
+
image: easy-skill-server:latest
|
|
86
|
+
ports:
|
|
87
|
+
- "8000:8000"
|
|
88
|
+
volumes:
|
|
89
|
+
- ./skills:/skills:ro
|
|
90
|
+
environment:
|
|
91
|
+
SKILLS_DIR: /skills
|
|
92
|
+
LOG_LEVEL: INFO
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
docker-compose up -d
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 📁 Estrutura de Skills
|
|
100
|
+
|
|
101
|
+
Segue o padrão [serpro-skills](https://agentskills.io) (Progressive Disclosure):
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
skills/
|
|
105
|
+
└── nome-da-skill/
|
|
106
|
+
├── SKILL.md # Obrigatório: Metadados + instruções
|
|
107
|
+
├── references/ # Opcional: Documentação de referência
|
|
108
|
+
├── scripts/ # Opcional: Scripts auxiliares
|
|
109
|
+
└── assets/ # Opcional: Templates, arquivos
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Exemplo de SKILL.md:
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
---
|
|
116
|
+
name: minha-skill
|
|
117
|
+
description: Descrição curta de quando usar esta skill
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
# Minha Skill
|
|
121
|
+
|
|
122
|
+
## Contexto e Objetivo
|
|
123
|
+
O que esta skill faz e qual problema resolve.
|
|
124
|
+
|
|
125
|
+
## Instruções de Uso
|
|
126
|
+
1. Passo 1
|
|
127
|
+
2. Passo 2
|
|
128
|
+
|
|
129
|
+
## Referências
|
|
130
|
+
- Veja [doc.md](references/doc.md) para detalhes
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Compatibilidade
|
|
134
|
+
|
|
135
|
+
Também suporta arquivos `.md` soltos (fallback) se nenhum diretório com `SKILL.md` for encontrado.
|
|
136
|
+
|
|
137
|
+
## 🔧 API Completa
|
|
138
|
+
|
|
139
|
+
### `create_server()`
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from skillserver import create_server
|
|
143
|
+
|
|
144
|
+
server = create_server(
|
|
145
|
+
skills_dir="./skills", # Diretório das skills
|
|
146
|
+
host="0.0.0.0", # Host (padrão: 0.0.0.0)
|
|
147
|
+
port=8000, # Porta (padrão: 8000)
|
|
148
|
+
name="MySkillServer", # Nome do servidor
|
|
149
|
+
version="1.0.0", # Versão
|
|
150
|
+
log_level="INFO", # DEBUG, INFO, WARNING, ERROR
|
|
151
|
+
run=True # Iniciar automaticamente
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `SkillServer` Class
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from skillserver import SkillServer
|
|
159
|
+
|
|
160
|
+
# Criar servidor sem iniciar automaticamente
|
|
161
|
+
server = SkillServer(
|
|
162
|
+
skills_dir="./skills",
|
|
163
|
+
name="MyServer",
|
|
164
|
+
version="1.0.0",
|
|
165
|
+
log_level="DEBUG"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Obter app ASGI para integração customizada
|
|
169
|
+
app = server.get_asgi_app()
|
|
170
|
+
|
|
171
|
+
# Iniciar servidor manualmente
|
|
172
|
+
server.run(host="0.0.0.0", port=8000)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 🐳 Docker
|
|
176
|
+
|
|
177
|
+
### Build Local
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
docker build -t easy-skill-server:latest .
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Configuração via Variáveis de Ambiente
|
|
184
|
+
|
|
185
|
+
| Variável | Descrição | Padrão |
|
|
186
|
+
|----------|-----------|--------|
|
|
187
|
+
| `SKILLS_DIR` | Diretório das skills | `/skills` |
|
|
188
|
+
| `SERVER_HOST` | Host para bind | `0.0.0.0` |
|
|
189
|
+
| `SERVER_PORT` | Porta do servidor | `8000` |
|
|
190
|
+
| `SERVER_NAME` | Nome do servidor | `SkillServer` |
|
|
191
|
+
| `SERVER_VERSION` | Versão do servidor | `0.1.0` |
|
|
192
|
+
| `LOG_LEVEL` | Nível de log | `INFO` |
|
|
193
|
+
|
|
194
|
+
### Healthcheck
|
|
195
|
+
|
|
196
|
+
A imagem inclui healthcheck automático:
|
|
197
|
+
```bash
|
|
198
|
+
docker ps # Verifica health status
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 🏗️ Arquitetura
|
|
202
|
+
|
|
203
|
+
O projeto segue princípios SOLID:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
src/skillserver/
|
|
207
|
+
├── __init__.py # API pública
|
|
208
|
+
├── server.py # SkillServer (Facade)
|
|
209
|
+
├── discovery.py # SkillDiscovery (Repository)
|
|
210
|
+
├── tools.py # SkillTools (Strategy)
|
|
211
|
+
├── models.py # Skill, SkillResource (Data)
|
|
212
|
+
└── utils.py # Funções utilitárias
|
|
213
|
+
|
|
214
|
+
entrypoint.py # Entrypoint Docker
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Princípios SOLID Aplicados
|
|
218
|
+
|
|
219
|
+
- **S**ingle Responsibility: Cada classe tem uma responsabilidade
|
|
220
|
+
- **O**pen/Closed: Extensível via herança
|
|
221
|
+
- **L**iskov Substitution: Interfaces claras
|
|
222
|
+
- **I**nterface Segregation: APIs focadas
|
|
223
|
+
- **D**ependency Inversion: Injeção de dependências
|
|
224
|
+
|
|
225
|
+
## 📚 Exemplos
|
|
226
|
+
|
|
227
|
+
Veja exemplos completos em [`examples/`](examples/):
|
|
228
|
+
- [`hello-world.md`](examples/basic/hello-world.md) - Skill básica
|
|
229
|
+
- [`documentation-helper.md`](examples/basic/documentation-helper.md) - Skill avançada
|
|
230
|
+
|
|
231
|
+
## 🧪 Desenvolvimento
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Clone o repositório
|
|
235
|
+
git clone https://git.serpro/vital/easy-skill-server.git
|
|
236
|
+
cd easy-skill-server
|
|
237
|
+
|
|
238
|
+
# Crie ambiente virtual
|
|
239
|
+
python -m venv venv
|
|
240
|
+
source venv/bin/activate # Linux/Mac
|
|
241
|
+
# ou
|
|
242
|
+
venv\Scripts\activate # Windows
|
|
243
|
+
|
|
244
|
+
# Instale em modo desenvolvimento
|
|
245
|
+
pip install -e ".[dev]"
|
|
246
|
+
# ou use o Makefile
|
|
247
|
+
make dev-install
|
|
248
|
+
|
|
249
|
+
# Execute testes
|
|
250
|
+
pytest
|
|
251
|
+
# ou
|
|
252
|
+
make test
|
|
253
|
+
|
|
254
|
+
# Testes com cobertura
|
|
255
|
+
make test-cov
|
|
256
|
+
|
|
257
|
+
# Formatação de código
|
|
258
|
+
black src/ tests/
|
|
259
|
+
ruff check src/ tests/
|
|
260
|
+
# ou
|
|
261
|
+
make format
|
|
262
|
+
|
|
263
|
+
# Type checking
|
|
264
|
+
mypy src/
|
|
265
|
+
# ou
|
|
266
|
+
make lint
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Comandos Make disponíveis
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
make help # Mostra todos os comandos disponíveis
|
|
273
|
+
make install # Instala o pacote localmente
|
|
274
|
+
make dev-install # Instala com dependências de dev
|
|
275
|
+
make test # Executa testes
|
|
276
|
+
make test-cov # Testes com cobertura
|
|
277
|
+
make lint # Executa linters
|
|
278
|
+
make format # Formata código
|
|
279
|
+
make clean # Limpa arquivos de build
|
|
280
|
+
make build # Gera pacotes de distribuição
|
|
281
|
+
make docker-build # Constrói imagem Docker
|
|
282
|
+
make docker-run # Executa container
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## 📦 Publicação
|
|
286
|
+
|
|
287
|
+
Para publicar no repositório Nexus do Serpro, consulte o guia completo em [PUBLISH.md](PUBLISH.md).
|
|
288
|
+
|
|
289
|
+
### Quick Start para Publicação:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# 1. Configure as credenciais (uma vez)
|
|
293
|
+
cp .pypirc.example ~/.pypirc
|
|
294
|
+
# Edite ~/.pypirc com suas credenciais
|
|
295
|
+
|
|
296
|
+
# 2. Atualize a versão (se necessário)
|
|
297
|
+
make version-patch # 0.1.0 -> 0.1.1
|
|
298
|
+
# ou
|
|
299
|
+
make version-minor # 0.1.0 -> 0.2.0
|
|
300
|
+
|
|
301
|
+
# 3. Publique
|
|
302
|
+
make release
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Para mais detalhes, incluindo configuração de CI/CD, troubleshooting e instalação a partir do Nexus, veja [PUBLISH.md](PUBLISH.md).
|
|
306
|
+
|
|
307
|
+
## 📖 Documentação Adicional
|
|
308
|
+
|
|
309
|
+
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io)
|
|
310
|
+
- [Agent Skills Standard](https://agentskills.io)
|
|
311
|
+
- [FastMCP Framework](https://github.com/jlowin/fastmcp)
|
|
312
|
+
|
|
313
|
+
## 📄 Licença
|
|
314
|
+
|
|
315
|
+
MIT License - veja [LICENSE](LICENSE) para detalhes.
|
|
316
|
+
|
|
317
|
+
## 🤝 Contribuindo
|
|
318
|
+
|
|
319
|
+
Contribuições são bem-vindas! Por favor:
|
|
320
|
+
|
|
321
|
+
1. Fork o projeto
|
|
322
|
+
2. Crie uma branch para sua feature (`git checkout -b feature/amazing`)
|
|
323
|
+
3. Commit suas mudanças (`git commit -m 'Add amazing feature'`)
|
|
324
|
+
4. Push para a branch (`git push origin feature/amazing`)
|
|
325
|
+
5. Abra um Pull Request
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
easy_skill_server-0.1.0.dist-info/licenses/LICENSE,sha256=iVl3kAJMX40N7eqQ1iuCxaJrGpS8LKPySSilg23A88w,1108
|
|
2
|
+
skillserver/__init__.py,sha256=X1joF0r8p-Krz6otMiduLo_9qWUirYmjOhvsTTKi6_I,639
|
|
3
|
+
skillserver/discovery.py,sha256=t1hIDYBy_VXqA4V7oQlfAMS_AsvogK65tz-l9W2IUd4,9640
|
|
4
|
+
skillserver/models.py,sha256=24Aw7k-4U7jjB8kBbkde0sXHpE-c45gl2OfyxiMUwNU,2237
|
|
5
|
+
skillserver/server.py,sha256=BAvnImAZ5TDS-jZLObfFoYrphO18yBYF7A2P5TIOpGU,6430
|
|
6
|
+
skillserver/tools.py,sha256=EZBgXMDIoU_nHnDx5Unz8SrHAJPzMq9h23aLi9TZ5fs,6292
|
|
7
|
+
skillserver/utils.py,sha256=4-dKL3h8cS8n-INFV9F4QTMoyEN9jiEpnyrXkHXnMnc,2772
|
|
8
|
+
easy_skill_server-0.1.0.dist-info/METADATA,sha256=RPoZbNJcSqSuRXRs1ajz-e1uVLChI7XaOO9EbowAK2Y,8264
|
|
9
|
+
easy_skill_server-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
easy_skill_server-0.1.0.dist-info/top_level.txt,sha256=3prbGi8MA7t4uR5xXfqMJJpJvQeTuZS2P7lKCmC5iDk,12
|
|
11
|
+
easy_skill_server-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serpro - Serviço Federal de Processamento de Dados
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
skillserver
|
skillserver/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SkillServer MCP - Biblioteca para servir Agent Skills via Model Context Protocol.
|
|
3
|
+
|
|
4
|
+
Esta biblioteca permite que qualquer pessoa suba um servidor de skills
|
|
5
|
+
facilmente usando MCP (Model Context Protocol) da Anthropic.
|
|
6
|
+
|
|
7
|
+
Exemplo de uso:
|
|
8
|
+
```python
|
|
9
|
+
from skillserver import create_server
|
|
10
|
+
|
|
11
|
+
# Criar e iniciar servidor
|
|
12
|
+
create_server(
|
|
13
|
+
skills_dir="./skills",
|
|
14
|
+
host="0.0.0.0",
|
|
15
|
+
port=8000
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .models import Skill, SkillResource
|
|
21
|
+
from .server import SkillServer, create_server
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Skill",
|
|
27
|
+
"SkillResource",
|
|
28
|
+
"SkillServer",
|
|
29
|
+
"create_server",
|
|
30
|
+
]
|
skillserver/discovery.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Descoberta de Skills no sistema de arquivos.
|
|
3
|
+
|
|
4
|
+
Seguindo o princípio Single Responsibility (SOLID), esta classe tem
|
|
5
|
+
apenas a responsabilidade de descobrir e carregar skills do disco.
|
|
6
|
+
|
|
7
|
+
A lógica de descoberta está isolada e pode ser facilmente testada
|
|
8
|
+
ou substituída por outra implementação (Open/Closed Principle).
|
|
9
|
+
|
|
10
|
+
Suporta dois formatos:
|
|
11
|
+
1. Padrão serpro-skills: Diretórios com SKILL.md + references/ + scripts/ + assets/
|
|
12
|
+
2. Formato simples: Arquivos .md soltos (fallback)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Iterator
|
|
18
|
+
|
|
19
|
+
from .models import Skill, SkillResource
|
|
20
|
+
from .utils import generate_skill_name, get_mime_type, parse_frontmatter
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SkillDiscovery:
|
|
26
|
+
"""
|
|
27
|
+
Responsável por descobrir e carregar Agent Skills de um diretório.
|
|
28
|
+
|
|
29
|
+
Esta classe implementa o padrão Repository, abstraindo a forma
|
|
30
|
+
como as skills são armazenadas e carregadas.
|
|
31
|
+
|
|
32
|
+
Suporta o padrão serpro-skills (agentskills.io):
|
|
33
|
+
- skills/nome-da-skill/SKILL.md (obrigatório)
|
|
34
|
+
- skills/nome-da-skill/references/ (opcional)
|
|
35
|
+
- skills/nome-da-skill/scripts/ (opcional)
|
|
36
|
+
- skills/nome-da-skill/assets/ (opcional)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Diretórios de recursos padrão do serpro-skills
|
|
40
|
+
RESOURCE_DIRS = ["references", "scripts", "assets"]
|
|
41
|
+
|
|
42
|
+
def __init__(self, skills_dir: Path):
|
|
43
|
+
"""
|
|
44
|
+
Inicializa o discovery com um diretório de skills.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
skills_dir: Diretório raiz contendo as skills
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: Se o diretório não existir
|
|
51
|
+
"""
|
|
52
|
+
self.skills_dir = Path(skills_dir)
|
|
53
|
+
if not self.skills_dir.exists():
|
|
54
|
+
raise ValueError(f"Skills directory not found: {self.skills_dir}")
|
|
55
|
+
if not self.skills_dir.is_dir():
|
|
56
|
+
raise ValueError(f"Path is not a directory: {self.skills_dir}")
|
|
57
|
+
|
|
58
|
+
def discover_skills(self) -> list[Skill]:
|
|
59
|
+
"""
|
|
60
|
+
Descobre todas as skills no diretório configurado.
|
|
61
|
+
|
|
62
|
+
Procura primeiro por diretórios com SKILL.md (padrão serpro-skills).
|
|
63
|
+
Se não encontrar nenhum, procura por arquivos .md soltos (fallback).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Lista de objetos Skill descobertos
|
|
67
|
+
|
|
68
|
+
Note:
|
|
69
|
+
Prioriza o padrão de diretórios sobre arquivos soltos para
|
|
70
|
+
compatibilidade com agentskills.io e serpro-skills.
|
|
71
|
+
"""
|
|
72
|
+
skills = []
|
|
73
|
+
|
|
74
|
+
# Primeiro tenta o padrão serpro-skills: diretórios com SKILL.md
|
|
75
|
+
skill_dirs = self._find_skill_directories()
|
|
76
|
+
|
|
77
|
+
if skill_dirs:
|
|
78
|
+
logger.info(f"Using serpro-skills pattern (SKILL.md in directories)")
|
|
79
|
+
for skill_dir in skill_dirs:
|
|
80
|
+
try:
|
|
81
|
+
skill_file = skill_dir / "SKILL.md"
|
|
82
|
+
skill = self._load_skill(skill_file)
|
|
83
|
+
skills.append(skill)
|
|
84
|
+
logger.info(f"Discovered skill: {skill.name} (from {skill_dir.name}/)")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to load skill from {skill_dir}: {e}")
|
|
87
|
+
continue
|
|
88
|
+
else:
|
|
89
|
+
# Fallback: busca arquivos .md soltos
|
|
90
|
+
logger.info(f"No SKILL.md found, trying simple .md files pattern")
|
|
91
|
+
for skill_file in self.skills_dir.rglob("*.md"):
|
|
92
|
+
# Ignora arquivos SKILL.md órfãos
|
|
93
|
+
if skill_file.name == "SKILL.md":
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
skill = self._load_skill(skill_file)
|
|
98
|
+
skills.append(skill)
|
|
99
|
+
logger.info(f"Discovered skill: {skill.name}")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Failed to load skill {skill_file}: {e}")
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if not skills:
|
|
105
|
+
logger.warning(f"No skills found in {self.skills_dir}")
|
|
106
|
+
|
|
107
|
+
return skills
|
|
108
|
+
|
|
109
|
+
def _find_skill_directories(self) -> list[Path]:
|
|
110
|
+
"""
|
|
111
|
+
Encontra diretórios que contêm SKILL.md.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Lista de diretórios que contêm um arquivo SKILL.md
|
|
115
|
+
"""
|
|
116
|
+
skill_dirs = []
|
|
117
|
+
|
|
118
|
+
# Busca recursivamente por arquivos SKILL.md
|
|
119
|
+
for skill_file in self.skills_dir.rglob("SKILL.md"):
|
|
120
|
+
skill_dir = skill_file.parent
|
|
121
|
+
# Evita diretórios escondidos ou especiais
|
|
122
|
+
if not any(part.startswith('.') or part.startswith('_')
|
|
123
|
+
for part in skill_dir.relative_to(self.skills_dir).parts):
|
|
124
|
+
skill_dirs.append(skill_dir)
|
|
125
|
+
|
|
126
|
+
return skill_dirs
|
|
127
|
+
|
|
128
|
+
def discover_resources(self, skill: Skill) -> list[SkillResource]:
|
|
129
|
+
"""
|
|
130
|
+
Descobre recursos estáticos associados a uma skill.
|
|
131
|
+
|
|
132
|
+
Segue o padrão serpro-skills/agentskills.io:
|
|
133
|
+
- references/: Documentação de referência, APIs, esquemas
|
|
134
|
+
- scripts/: Scripts executáveis (Python, Bash, etc)
|
|
135
|
+
- assets/: Templates, ícones, arquivos base
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
skill: Skill para buscar recursos
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Lista de recursos descobertos
|
|
142
|
+
"""
|
|
143
|
+
resources = []
|
|
144
|
+
skill_dir = skill.file_path.parent
|
|
145
|
+
|
|
146
|
+
# Busca nos diretórios padrão do serpro-skills
|
|
147
|
+
for resource_dir_name in self.RESOURCE_DIRS:
|
|
148
|
+
resource_dir = skill_dir / resource_dir_name
|
|
149
|
+
|
|
150
|
+
if resource_dir.exists() and resource_dir.is_dir():
|
|
151
|
+
# Busca recursivamente todos os arquivos
|
|
152
|
+
for file_path in resource_dir.rglob("*"):
|
|
153
|
+
if file_path.is_file():
|
|
154
|
+
try:
|
|
155
|
+
resource = self._create_resource(
|
|
156
|
+
skill,
|
|
157
|
+
file_path,
|
|
158
|
+
resource_type=resource_dir_name
|
|
159
|
+
)
|
|
160
|
+
resources.append(resource)
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Discovered {resource_dir_name} resource: "
|
|
163
|
+
f"{file_path.name} for skill {skill.name}"
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to load resource {file_path}: {e}")
|
|
167
|
+
|
|
168
|
+
# Também busca arquivos soltos no diretório da skill (exceto SKILL.md)
|
|
169
|
+
for file_path in skill_dir.iterdir():
|
|
170
|
+
if (file_path.is_file() and
|
|
171
|
+
file_path.name != "SKILL.md" and
|
|
172
|
+
file_path.suffix != ".md"):
|
|
173
|
+
try:
|
|
174
|
+
resource = self._create_resource(skill, file_path)
|
|
175
|
+
resources.append(resource)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to load resource {file_path}: {e}")
|
|
178
|
+
|
|
179
|
+
if resources:
|
|
180
|
+
logger.info(f"Discovered {len(resources)} resources for skill {skill.name}")
|
|
181
|
+
|
|
182
|
+
return resources
|
|
183
|
+
|
|
184
|
+
def _load_skill(self, file_path: Path) -> Skill:
|
|
185
|
+
"""
|
|
186
|
+
Carrega uma skill de um arquivo markdown.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
file_path: Caminho do arquivo .md
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Objeto Skill carregado
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
ValueError: Se o arquivo não puder ser parseado
|
|
196
|
+
"""
|
|
197
|
+
# Lê o conteúdo do arquivo
|
|
198
|
+
content = file_path.read_text(encoding="utf-8")
|
|
199
|
+
|
|
200
|
+
# Parse frontmatter e conteúdo
|
|
201
|
+
metadata, instructions = parse_frontmatter(content)
|
|
202
|
+
|
|
203
|
+
# Extrai informações da skill
|
|
204
|
+
skill_name = metadata.get("name") or generate_skill_name(file_path)
|
|
205
|
+
title = metadata.get("title") or skill_name.replace("-", " ").title()
|
|
206
|
+
description = metadata.get("description") or title
|
|
207
|
+
|
|
208
|
+
# Cria objeto Skill
|
|
209
|
+
return Skill(
|
|
210
|
+
name=skill_name,
|
|
211
|
+
title=title,
|
|
212
|
+
description=description,
|
|
213
|
+
instructions=instructions.strip(),
|
|
214
|
+
file_path=file_path,
|
|
215
|
+
metadata=metadata,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _create_resource(
|
|
219
|
+
self,
|
|
220
|
+
skill: Skill,
|
|
221
|
+
file_path: Path,
|
|
222
|
+
resource_type: str | None = None
|
|
223
|
+
) -> SkillResource:
|
|
224
|
+
"""
|
|
225
|
+
Cria um objeto SkillResource para um arquivo.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
skill: Skill dona do recurso
|
|
229
|
+
file_path: Caminho do arquivo do recurso
|
|
230
|
+
resource_type: Tipo do recurso (references, scripts, assets) ou None
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Objeto SkillResource criado
|
|
234
|
+
"""
|
|
235
|
+
skill_dir = skill.file_path.parent
|
|
236
|
+
|
|
237
|
+
# Gera URI única para o recurso
|
|
238
|
+
relative_path = file_path.relative_to(skill_dir)
|
|
239
|
+
uri = f"skill://{skill.name}/{relative_path}"
|
|
240
|
+
|
|
241
|
+
# Determina tipo MIME
|
|
242
|
+
mime_type = get_mime_type(file_path)
|
|
243
|
+
|
|
244
|
+
# Gera descrição baseada no tipo de recurso
|
|
245
|
+
if resource_type == "references":
|
|
246
|
+
description = f"Reference documentation: {file_path.name}"
|
|
247
|
+
elif resource_type == "scripts":
|
|
248
|
+
description = f"Script: {file_path.name}"
|
|
249
|
+
elif resource_type == "assets":
|
|
250
|
+
description = f"Asset file: {file_path.name}"
|
|
251
|
+
else:
|
|
252
|
+
description = f"Resource for {skill.title}: {file_path.name}"
|
|
253
|
+
|
|
254
|
+
return SkillResource(
|
|
255
|
+
uri=uri,
|
|
256
|
+
name=file_path.name,
|
|
257
|
+
skill_name=skill.name,
|
|
258
|
+
file_path=file_path,
|
|
259
|
+
mime_type=mime_type,
|
|
260
|
+
description=description,
|
|
261
|
+
)
|
skillserver/models.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modelos de dados para Skills e Resources.
|
|
3
|
+
|
|
4
|
+
Seguindo o princípio Single Responsibility (SOLID), este módulo contém
|
|
5
|
+
apenas as definições de dados, sem lógica de negócio.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Skill:
|
|
15
|
+
"""
|
|
16
|
+
Representa uma Agent Skill descoberta no sistema.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
name: Nome único da skill (ex: "hello-world")
|
|
20
|
+
title: Título legível da skill
|
|
21
|
+
description: Descrição breve da skill
|
|
22
|
+
instructions: Instruções completas em markdown
|
|
23
|
+
file_path: Caminho do arquivo da skill
|
|
24
|
+
metadata: Metadados adicionais do frontmatter YAML
|
|
25
|
+
"""
|
|
26
|
+
name: str
|
|
27
|
+
title: str
|
|
28
|
+
description: str
|
|
29
|
+
instructions: str
|
|
30
|
+
file_path: Path
|
|
31
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
"""Valida e normaliza os dados após inicialização."""
|
|
35
|
+
if not self.name:
|
|
36
|
+
raise ValueError("Skill name cannot be empty")
|
|
37
|
+
if not self.title:
|
|
38
|
+
self.title = self.name.replace("-", " ").title()
|
|
39
|
+
if not self.description:
|
|
40
|
+
self.description = self.title
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SkillResource:
|
|
45
|
+
"""
|
|
46
|
+
Representa um recurso estático associado a uma skill.
|
|
47
|
+
|
|
48
|
+
Resources podem ser imagens, arquivos de dados, ou qualquer outro
|
|
49
|
+
arquivo que a skill precise disponibilizar para o agente.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
uri: URI única do recurso (ex: "skill://hello-world/image.png")
|
|
53
|
+
name: Nome do arquivo
|
|
54
|
+
skill_name: Nome da skill dona do recurso
|
|
55
|
+
file_path: Caminho físico do arquivo
|
|
56
|
+
mime_type: Tipo MIME do recurso
|
|
57
|
+
description: Descrição opcional do recurso
|
|
58
|
+
"""
|
|
59
|
+
uri: str
|
|
60
|
+
name: str
|
|
61
|
+
skill_name: str
|
|
62
|
+
file_path: Path
|
|
63
|
+
mime_type: str = "application/octet-stream"
|
|
64
|
+
description: str = ""
|
|
65
|
+
|
|
66
|
+
def __post_init__(self):
|
|
67
|
+
"""Valida os dados após inicialização."""
|
|
68
|
+
if not self.uri.startswith("skill://"):
|
|
69
|
+
raise ValueError(f"Resource URI must start with 'skill://': {self.uri}")
|
|
70
|
+
if not self.file_path.exists():
|
|
71
|
+
raise FileNotFoundError(f"Resource file not found: {self.file_path}")
|
skillserver/server.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Servidor MCP principal para Agent Skills.
|
|
3
|
+
|
|
4
|
+
Este módulo implementa o servidor MCP que orquestra todos os componentes
|
|
5
|
+
seguindo os princípios SOLID:
|
|
6
|
+
|
|
7
|
+
- Single Responsibility: SkillServer apenas orquestra componentes
|
|
8
|
+
- Open/Closed: Extensível via herança, fechado para modificação
|
|
9
|
+
- Liskov Substitution: Pode ser substituído por subclasses
|
|
10
|
+
- Interface Segregation: Interfaces pequenas e focadas
|
|
11
|
+
- Dependency Inversion: Depende de abstrações (SkillDiscovery, SkillTools)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import uvicorn
|
|
18
|
+
from fastmcp import FastMCP
|
|
19
|
+
|
|
20
|
+
from .discovery import SkillDiscovery
|
|
21
|
+
from .models import Skill, SkillResource
|
|
22
|
+
from .tools import SkillTools
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SkillServer:
|
|
28
|
+
"""
|
|
29
|
+
Servidor MCP para servir Agent Skills via HTTP/SSE.
|
|
30
|
+
|
|
31
|
+
Esta classe implementa o padrão Facade, fornecendo uma interface
|
|
32
|
+
simples para um subsistema complexo (MCP + Skills + Resources).
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
skills_dir: Diretório contendo as skills
|
|
36
|
+
name: Nome do servidor MCP
|
|
37
|
+
version: Versão do servidor
|
|
38
|
+
mcp: Instância do FastMCP
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
skills_dir: str | Path,
|
|
44
|
+
name: str = "SkillServer",
|
|
45
|
+
version: str = "0.1.0",
|
|
46
|
+
log_level: str = "INFO",
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Inicializa o servidor MCP.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
skills_dir: Diretório contendo as skills
|
|
53
|
+
name: Nome do servidor MCP
|
|
54
|
+
version: Versão do servidor
|
|
55
|
+
log_level: Nível de logging (DEBUG, INFO, WARNING, ERROR)
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: Se o diretório de skills não existir
|
|
59
|
+
"""
|
|
60
|
+
self.skills_dir = Path(skills_dir)
|
|
61
|
+
self.name = name
|
|
62
|
+
self.version = version
|
|
63
|
+
|
|
64
|
+
# Setup de logging
|
|
65
|
+
self._setup_logging(log_level)
|
|
66
|
+
|
|
67
|
+
# Inicializa FastMCP
|
|
68
|
+
self.mcp = FastMCP(name=name, version=version)
|
|
69
|
+
|
|
70
|
+
# Inicializa componentes (Dependency Injection)
|
|
71
|
+
self.discovery = SkillDiscovery(self.skills_dir)
|
|
72
|
+
self.tools = SkillTools(self.mcp)
|
|
73
|
+
|
|
74
|
+
# Descobre e registra skills
|
|
75
|
+
self._discover_and_register()
|
|
76
|
+
|
|
77
|
+
logger.info(f"{name} v{version} initialized")
|
|
78
|
+
|
|
79
|
+
def _setup_logging(self, log_level: str) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Configura o sistema de logging.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
log_level: Nível de logging desejado
|
|
85
|
+
"""
|
|
86
|
+
numeric_level = getattr(logging, log_level.upper(), None)
|
|
87
|
+
if not isinstance(numeric_level, int):
|
|
88
|
+
numeric_level = logging.INFO
|
|
89
|
+
|
|
90
|
+
logging.basicConfig(
|
|
91
|
+
level=numeric_level,
|
|
92
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _discover_and_register(self) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Descobre skills e recursos, registrando-os no MCP.
|
|
98
|
+
|
|
99
|
+
Este método orquestra o processo de descoberta e registro,
|
|
100
|
+
delegando responsabilidades para os componentes especializados.
|
|
101
|
+
"""
|
|
102
|
+
# Descobre skills
|
|
103
|
+
skills = self.discovery.discover_skills()
|
|
104
|
+
|
|
105
|
+
if not skills:
|
|
106
|
+
logger.warning("No skills found - server will start but have no tools")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Registra skills como ferramentas MCP
|
|
110
|
+
self.tools.register_skills(skills)
|
|
111
|
+
|
|
112
|
+
# Descobre e registra recursos
|
|
113
|
+
all_resources = []
|
|
114
|
+
for skill in skills:
|
|
115
|
+
resources = self.discovery.discover_resources(skill)
|
|
116
|
+
all_resources.extend(resources)
|
|
117
|
+
|
|
118
|
+
if all_resources:
|
|
119
|
+
self.tools.register_resources(all_resources)
|
|
120
|
+
|
|
121
|
+
logger.info(
|
|
122
|
+
f"Discovery complete: {len(skills)} skills, "
|
|
123
|
+
f"{len(all_resources)} resources"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def get_asgi_app(self):
|
|
127
|
+
"""
|
|
128
|
+
Retorna a aplicação ASGI do FastMCP.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Aplicação ASGI pronta para uso com uvicorn ou outro servidor
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
Este método permite usar o servidor com outros ASGI servers
|
|
135
|
+
ou integrá-lo em aplicações existentes.
|
|
136
|
+
"""
|
|
137
|
+
return self.mcp.http_app()
|
|
138
|
+
|
|
139
|
+
def run(self, host: str = "0.0.0.0", port: int = 8000) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Inicia o servidor MCP.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
host: Endereço para bind (padrão: 0.0.0.0)
|
|
145
|
+
port: Porta para o servidor (padrão: 8000)
|
|
146
|
+
|
|
147
|
+
Note:
|
|
148
|
+
Este método bloqueia a execução até o servidor ser encerrado.
|
|
149
|
+
"""
|
|
150
|
+
logger.info(f"Starting MCP server on {host}:{port}")
|
|
151
|
+
logger.info(f"Skills directory: {self.skills_dir.absolute()}")
|
|
152
|
+
logger.info(f"Serving {len(self.tools.skills)} skills")
|
|
153
|
+
|
|
154
|
+
# Usa uvicorn para servir a aplicação ASGI
|
|
155
|
+
uvicorn.run(
|
|
156
|
+
self.get_asgi_app(),
|
|
157
|
+
host=host,
|
|
158
|
+
port=port,
|
|
159
|
+
log_level="info",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_server(
|
|
164
|
+
skills_dir: str | Path,
|
|
165
|
+
host: str = "0.0.0.0",
|
|
166
|
+
port: int = 8000,
|
|
167
|
+
name: str = "SkillServer",
|
|
168
|
+
version: str = "0.1.0",
|
|
169
|
+
log_level: str = "INFO",
|
|
170
|
+
run: bool = True,
|
|
171
|
+
) -> SkillServer:
|
|
172
|
+
"""
|
|
173
|
+
Função de conveniência para criar e opcionalmente iniciar um servidor MCP.
|
|
174
|
+
|
|
175
|
+
Esta é a API pública principal da biblioteca, fornecendo uma forma
|
|
176
|
+
simples e direta de criar servidores de skills.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
skills_dir: Diretório contendo as skills
|
|
180
|
+
host: Endereço para bind (padrão: 0.0.0.0)
|
|
181
|
+
port: Porta para o servidor (padrão: 8000)
|
|
182
|
+
name: Nome do servidor MCP
|
|
183
|
+
version: Versão do servidor
|
|
184
|
+
log_level: Nível de logging (DEBUG, INFO, WARNING, ERROR)
|
|
185
|
+
run: Se True, inicia o servidor imediatamente
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Instância do SkillServer criado
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> # Criar e iniciar servidor
|
|
192
|
+
>>> create_server("./skills")
|
|
193
|
+
|
|
194
|
+
>>> # Criar servidor sem iniciar (para testes ou uso customizado)
|
|
195
|
+
>>> server = create_server("./skills", run=False)
|
|
196
|
+
>>> app = server.get_asgi_app()
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: Se o diretório de skills não existir
|
|
200
|
+
"""
|
|
201
|
+
server = SkillServer(
|
|
202
|
+
skills_dir=skills_dir,
|
|
203
|
+
name=name,
|
|
204
|
+
version=version,
|
|
205
|
+
log_level=log_level,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if run:
|
|
209
|
+
server.run(host=host, port=port)
|
|
210
|
+
|
|
211
|
+
return server
|
skillserver/tools.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ferramentas MCP para interação com Agent Skills.
|
|
3
|
+
|
|
4
|
+
Seguindo o princípio Single Responsibility, esta classe é responsável
|
|
5
|
+
apenas por registrar as ferramentas (tools) do MCP que permitem
|
|
6
|
+
aos agentes interagirem com as skills.
|
|
7
|
+
|
|
8
|
+
Dependency Inversion: Esta classe depende de abstrações (Skill, SkillResource)
|
|
9
|
+
ao invés de implementações concretas.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
from .models import Skill, SkillResource
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SkillTools:
|
|
24
|
+
"""
|
|
25
|
+
Registra e gerencia as ferramentas MCP para acesso às skills.
|
|
26
|
+
|
|
27
|
+
Esta classe segue o padrão Strategy, encapsulando a lógica de
|
|
28
|
+
registro de ferramentas e permitindo que seja facilmente testada
|
|
29
|
+
ou substituída.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, mcp: FastMCP):
|
|
33
|
+
"""
|
|
34
|
+
Inicializa o gerenciador de ferramentas.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
mcp: Instância do FastMCP onde registrar as ferramentas
|
|
38
|
+
"""
|
|
39
|
+
self.mcp = mcp
|
|
40
|
+
self.skills: dict[str, Skill] = {}
|
|
41
|
+
self.resources: dict[str, SkillResource] = {}
|
|
42
|
+
|
|
43
|
+
def register_skills(self, skills: list[Skill]) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Registra uma lista de skills e suas ferramentas.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
skills: Lista de skills a serem registradas
|
|
49
|
+
"""
|
|
50
|
+
for skill in skills:
|
|
51
|
+
self.skills[skill.name] = skill
|
|
52
|
+
|
|
53
|
+
self._register_tools()
|
|
54
|
+
logger.info(f"Registered {len(skills)} skills with MCP tools")
|
|
55
|
+
|
|
56
|
+
def register_resources(self, resources: list[SkillResource]) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Registra recursos estáticos das skills.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
resources: Lista de recursos a serem registrados
|
|
62
|
+
"""
|
|
63
|
+
for resource in resources:
|
|
64
|
+
self.resources[resource.uri] = resource
|
|
65
|
+
# Registra cada recurso como um MCP resource
|
|
66
|
+
self._register_resource(resource)
|
|
67
|
+
|
|
68
|
+
logger.info(f"Registered {len(resources)} skill resources")
|
|
69
|
+
|
|
70
|
+
def _register_tools(self) -> None:
|
|
71
|
+
"""Registra as ferramentas MCP principais."""
|
|
72
|
+
|
|
73
|
+
@self.mcp.tool()
|
|
74
|
+
def list_skills() -> list[dict[str, str]]:
|
|
75
|
+
"""
|
|
76
|
+
Lista todas as Agent Skills disponíveis.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Lista de skills com name, title e description
|
|
80
|
+
"""
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
"name": skill.name,
|
|
84
|
+
"title": skill.title,
|
|
85
|
+
"description": skill.description,
|
|
86
|
+
}
|
|
87
|
+
for skill in self.skills.values()
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
@self.mcp.tool()
|
|
91
|
+
def get_skill_instructions(skill_name: str) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Obtém as instruções completas de uma skill específica.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
skill_name: Nome da skill (ex: "hello-world")
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Instruções em markdown da skill
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: Se a skill não existir
|
|
103
|
+
"""
|
|
104
|
+
skill = self.skills.get(skill_name)
|
|
105
|
+
if not skill:
|
|
106
|
+
available = ", ".join(self.skills.keys())
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Skill '{skill_name}' not found. "
|
|
109
|
+
f"Available skills: {available}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return skill.instructions
|
|
113
|
+
|
|
114
|
+
@self.mcp.tool()
|
|
115
|
+
def search_skills(query: str) -> list[dict[str, str]]:
|
|
116
|
+
"""
|
|
117
|
+
Busca skills por palavra-chave no título ou descrição.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
query: Texto para buscar
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Lista de skills que correspondem à busca
|
|
124
|
+
"""
|
|
125
|
+
query_lower = query.lower()
|
|
126
|
+
results = []
|
|
127
|
+
|
|
128
|
+
for skill in self.skills.values():
|
|
129
|
+
if (query_lower in skill.title.lower() or
|
|
130
|
+
query_lower in skill.description.lower() or
|
|
131
|
+
query_lower in skill.name.lower()):
|
|
132
|
+
results.append({
|
|
133
|
+
"name": skill.name,
|
|
134
|
+
"title": skill.title,
|
|
135
|
+
"description": skill.description,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return results
|
|
139
|
+
|
|
140
|
+
@self.mcp.tool()
|
|
141
|
+
def get_skill_metadata(skill_name: str) -> dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Obtém metadados completos de uma skill.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
skill_name: Nome da skill
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dicionário com todos os metadados da skill
|
|
150
|
+
"""
|
|
151
|
+
skill = self.skills.get(skill_name)
|
|
152
|
+
if not skill:
|
|
153
|
+
raise ValueError(f"Skill '{skill_name}' not found")
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"name": skill.name,
|
|
157
|
+
"title": skill.title,
|
|
158
|
+
"description": skill.description,
|
|
159
|
+
"file_path": str(skill.file_path),
|
|
160
|
+
"metadata": skill.metadata,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def _register_resource(self, resource: SkillResource) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Registra um recurso individual como MCP resource.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
resource: Recurso a ser registrado
|
|
169
|
+
"""
|
|
170
|
+
# Para recursos de texto, podemos disponibilizar o conteúdo diretamente
|
|
171
|
+
if resource.mime_type.startswith("text/"):
|
|
172
|
+
@self.mcp.resource(resource.uri)
|
|
173
|
+
def get_text_resource() -> str:
|
|
174
|
+
"""Retorna conteúdo de recurso de texto."""
|
|
175
|
+
return resource.file_path.read_text(encoding="utf-8")
|
|
176
|
+
else:
|
|
177
|
+
# Para recursos binários, disponibilizamos metadados
|
|
178
|
+
@self.mcp.resource(resource.uri)
|
|
179
|
+
def get_binary_resource() -> dict[str, str]:
|
|
180
|
+
"""Retorna metadados de recurso binário."""
|
|
181
|
+
return {
|
|
182
|
+
"uri": resource.uri,
|
|
183
|
+
"name": resource.name,
|
|
184
|
+
"mime_type": resource.mime_type,
|
|
185
|
+
"description": resource.description,
|
|
186
|
+
"note": "Binary resource - download via file system or API",
|
|
187
|
+
}
|
skillserver/utils.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Funções utilitárias para parsing e manipulação de arquivos.
|
|
3
|
+
|
|
4
|
+
Seguindo o princípio Single Responsibility, cada função tem uma
|
|
5
|
+
responsabilidade específica e bem definida.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import mimetypes
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
|
|
17
|
+
"""
|
|
18
|
+
Extrai frontmatter YAML e conteúdo markdown de um arquivo.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
content: Conteúdo completo do arquivo markdown
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tupla com (metadados, conteúdo_markdown)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> content = '''---
|
|
28
|
+
... title: My Skill
|
|
29
|
+
... ---
|
|
30
|
+
... # Content here'''
|
|
31
|
+
>>> metadata, body = parse_frontmatter(content)
|
|
32
|
+
>>> metadata['title']
|
|
33
|
+
'My Skill'
|
|
34
|
+
"""
|
|
35
|
+
# Regex para capturar frontmatter YAML
|
|
36
|
+
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
|
|
37
|
+
match = re.match(pattern, content, re.DOTALL)
|
|
38
|
+
|
|
39
|
+
if not match:
|
|
40
|
+
return {}, content
|
|
41
|
+
|
|
42
|
+
yaml_content = match.group(1)
|
|
43
|
+
markdown_content = match.group(2)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
metadata = yaml.safe_load(yaml_content) or {}
|
|
47
|
+
except yaml.YAMLError as e:
|
|
48
|
+
raise ValueError(f"Invalid YAML frontmatter: {e}")
|
|
49
|
+
|
|
50
|
+
return metadata, markdown_content
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_mime_type(file_path: Path) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Determina o tipo MIME de um arquivo baseado na extensão.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
file_path: Caminho do arquivo
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
String com o tipo MIME (ex: "image/png", "text/plain")
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> get_mime_type(Path("image.png"))
|
|
65
|
+
'image/png'
|
|
66
|
+
"""
|
|
67
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
68
|
+
return mime_type or "application/octet-stream"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_text_file(file_path: Path) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Verifica se um arquivo é de texto ou binário.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
file_path: Caminho do arquivo
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True se for arquivo de texto, False caso contrário
|
|
80
|
+
"""
|
|
81
|
+
mime_type = get_mime_type(file_path)
|
|
82
|
+
return mime_type.startswith("text/") or mime_type in [
|
|
83
|
+
"application/json",
|
|
84
|
+
"application/xml",
|
|
85
|
+
"application/javascript",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generate_skill_name(file_path: Path) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Gera um nome único para a skill baseado no caminho do arquivo.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
file_path: Caminho do arquivo da skill
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Nome da skill (ex: "hello-world", "category/advanced-skill")
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> generate_skill_name(Path("skills/hello-world.md"))
|
|
101
|
+
'hello-world'
|
|
102
|
+
"""
|
|
103
|
+
# Remove extensão e normaliza o nome
|
|
104
|
+
name = file_path.stem
|
|
105
|
+
# Converte para kebab-case
|
|
106
|
+
name = re.sub(r'[^a-z0-9]+', '-', name.lower())
|
|
107
|
+
name = name.strip('-')
|
|
108
|
+
return name or "unnamed-skill"
|