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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
@@ -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
+ ]
@@ -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"