systemlink-cli 1.27.7__tar.gz → 1.29.1__tar.gz
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.
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/PKG-INFO +1 -1
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/pyproject.toml +2 -1
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/_version.py +1 -1
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/mcp_server.py +2 -72
- systemlink_cli-1.29.1/slcli/mcp_skills.py +490 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/COPILOT_BATCH_RUN_PROMPT.md +127 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/README.md +236 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/evals.json +642 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/evals.schema.json +53 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/files/spec-import/battery-specs.csv +4 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/files/webapp-package/icon.svg +4 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/files/webapp-package/nipkg.config.json +15 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/files/webapp-package/webapp-content/index.html +10 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/evals/trigger_evals.json +34 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/benchmark_iteration.py +183 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/compare_iteration.py +350 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/eval_manifest.py +134 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/grade_eval_response.py +354 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/grade_iteration.py +283 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/prepare_eval_prompts.py +279 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/prepare_eval_workspace.py +523 -0
- systemlink_cli-1.29.1/slcli/skills/slcli/scripts/render_eval_review.py +82 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/LICENSE +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/dff-editor/index.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/__init__.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/__main__.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/alarm_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/config.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/config_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/dataframe_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/example_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-1/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-1/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-2/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-2/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/leakage-current-results.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/overvoltage-results.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/product-xyz-compliance-report.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/product-xyz-specification.csv +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/demo-data-3/thermal-response-results.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/file_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/function_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/main.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/mcp_reachability.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/pkce.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/platform.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/profiles.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/rich_output.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/SKILL.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/commands.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/datasheet-workflow.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/example-authoring.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/import-specs.min.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/job-debugging/overview.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/nipkg/overview.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/notebook/interfaces.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/notebook/notebook-patterns.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/notebook/overview.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/python-test/overview.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/routine-examples.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/troubleshooting.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/angular-ui-packages.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/deployment.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/layout-patterns.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/nimble-angular.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/overview.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/systemlink-services.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/references/webapp/troubleshooting.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/skills/slcli/scripts/spec_import_helper.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/spec_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/state_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/system_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/system_query_utils.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/systemlink-logo.svg +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/user_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/utils.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/version_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_bootstrap.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/.gitignore +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/angular.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/package.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/scripts/filter-sbom.js +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/app-routing.module.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/app.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/app.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/app.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/app.module.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/layout/app-shell.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/state/app-view-state.service.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/systemlink-context.service.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/theme-sync.service.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/core/systemlink/webapp-home-data.service.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/assets/assets-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/datasets/datasets-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/home/home-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/master-detail/master-detail-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/operations/operations-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/features/settings/settings-page.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/empty-state.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/error-banner.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/components/loading-state.component.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/app/shared/states/view-state.model.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/assets/README.md +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/index.html +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/main.ts +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/src/styles.scss +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/tsconfig.app.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/webapp_templates/angular/blank/tsconfig.json +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.27.7 → systemlink_cli-1.29.1}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.29.1"
|
|
4
4
|
description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
|
|
5
5
|
authors = ["Fred Visser <fred.visser@emerson.com>"]
|
|
6
6
|
packages = [{ include = "slcli" }]
|
|
@@ -68,6 +68,7 @@ pytest = ">=7.0"
|
|
|
68
68
|
pytest-cov = ">=3.0"
|
|
69
69
|
pytest-timeout = ">=2.1.0"
|
|
70
70
|
pyinstaller = "^6.14.2"
|
|
71
|
+
jsonschema = ">=4.0"
|
|
71
72
|
|
|
72
73
|
# Typed stubs for third-party libraries used by mypy in CI
|
|
73
74
|
types-requests = "*"
|
|
@@ -8,7 +8,6 @@ import asyncio
|
|
|
8
8
|
import json
|
|
9
9
|
import sys
|
|
10
10
|
import urllib.parse
|
|
11
|
-
from pathlib import Path
|
|
12
11
|
from typing import Annotated, Any, Callable, Dict, List, Literal, Optional, TypeVar
|
|
13
12
|
|
|
14
13
|
from mcp.server.mcpserver import MCPServer
|
|
@@ -16,6 +15,7 @@ from mcp.types import ToolAnnotations
|
|
|
16
15
|
from pydantic import BaseModel, Field
|
|
17
16
|
|
|
18
17
|
from ._version import __version__
|
|
18
|
+
from .mcp_skills import SlcliSkillsExtension
|
|
19
19
|
|
|
20
20
|
server = MCPServer(
|
|
21
21
|
name="slcli",
|
|
@@ -29,6 +29,7 @@ server = MCPServer(
|
|
|
29
29
|
"visible data was found; it does not prove that a resource does not exist."
|
|
30
30
|
),
|
|
31
31
|
version=__version__,
|
|
32
|
+
extensions=[SlcliSkillsExtension()],
|
|
32
33
|
)
|
|
33
34
|
T = TypeVar("T")
|
|
34
35
|
|
|
@@ -40,29 +41,6 @@ _READ_ONLY_TOOL_ANNOTATIONS = ToolAnnotations(
|
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def _reference_root_candidates() -> List[Path]:
|
|
44
|
-
"""Return candidate packaged-reference directories for source and frozen layouts."""
|
|
45
|
-
candidates: List[Path] = []
|
|
46
|
-
|
|
47
|
-
meipass = getattr(sys, "_MEIPASS", None)
|
|
48
|
-
if meipass:
|
|
49
|
-
candidates.append(Path(meipass) / "skills" / "slcli" / "references")
|
|
50
|
-
|
|
51
|
-
if getattr(sys, "frozen", False):
|
|
52
|
-
candidates.append(Path(sys.executable).resolve().parent / "skills" / "slcli" / "references")
|
|
53
|
-
|
|
54
|
-
candidates.append(Path(__file__).resolve().parent / "skills" / "slcli" / "references")
|
|
55
|
-
return candidates
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _find_reference_root() -> Path:
|
|
59
|
-
"""Locate the packaged MCP reference directory."""
|
|
60
|
-
for candidate in _reference_root_candidates():
|
|
61
|
-
if candidate.is_dir():
|
|
62
|
-
return candidate
|
|
63
|
-
raise FileNotFoundError("Bundled MCP reference files not found.")
|
|
64
|
-
|
|
65
|
-
|
|
66
44
|
class WorkspaceQueryResponse(BaseModel):
|
|
67
45
|
"""Structured response returned by workspace discovery."""
|
|
68
46
|
|
|
@@ -70,54 +48,6 @@ class WorkspaceQueryResponse(BaseModel):
|
|
|
70
48
|
count: int = Field(description="Number of workspaces in items.")
|
|
71
49
|
|
|
72
50
|
|
|
73
|
-
_CAPABILITIES = """# slcli MCP capabilities
|
|
74
|
-
|
|
75
|
-
The server exposes query-oriented tools for the main SystemLink resource types.
|
|
76
|
-
|
|
77
|
-
Core discovery tools:
|
|
78
|
-
- query_workspaces, query_users
|
|
79
|
-
- search_tags, read_tag_values, get_tag_by_path, query_tag_history
|
|
80
|
-
- query_systems, query_assets, query_alarms
|
|
81
|
-
- query_test_results, get_test_steps
|
|
82
|
-
- query_files, query_notebooks
|
|
83
|
-
- query_workitems, query_workitem_templates, query_workflows
|
|
84
|
-
- query_feeds, query_feed_packages
|
|
85
|
-
- query_webapps
|
|
86
|
-
- query_policies, query_comments
|
|
87
|
-
|
|
88
|
-
Use the corresponding get_* tool when you already know the resource ID/path.
|
|
89
|
-
Most query tools support a small set of structured filters plus a raw service
|
|
90
|
-
filter when the underlying API supports it.
|
|
91
|
-
|
|
92
|
-
Detailed references are available as read-only resources:
|
|
93
|
-
- slcli://docs/commands
|
|
94
|
-
- slcli://docs/filtering
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@server.resource("slcli://capabilities")
|
|
99
|
-
def capabilities() -> str:
|
|
100
|
-
"""Return the tool-selection rules and reference resource index."""
|
|
101
|
-
return _CAPABILITIES
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _read_reference(filename: str) -> str:
|
|
105
|
-
"""Read a packaged slcli skill reference for an MCP resource."""
|
|
106
|
-
return (_find_reference_root() / filename).read_text(encoding="utf-8")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@server.resource("slcli://docs/commands", mime_type="text/markdown")
|
|
110
|
-
def commands_reference() -> str:
|
|
111
|
-
"""Return the complete slcli command and resource reference."""
|
|
112
|
-
return _read_reference("commands.md")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@server.resource("slcli://docs/filtering", mime_type="text/markdown")
|
|
116
|
-
def filtering_reference() -> str:
|
|
117
|
-
"""Return filtering and substitution guidance for SystemLink services."""
|
|
118
|
-
return _read_reference("filtering.md")
|
|
119
|
-
|
|
120
|
-
|
|
121
51
|
@server.prompt(
|
|
122
52
|
name="investigate_failed_test_results",
|
|
123
53
|
title="Investigate failed test results",
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""MCP Skills extension support for the bundled slcli Agent Skill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Awaitable, Callable, Dict, List, Mapping, Optional, Tuple
|
|
12
|
+
from urllib.parse import unquote, urlsplit
|
|
13
|
+
|
|
14
|
+
import yaml # type: ignore[import-untyped]
|
|
15
|
+
from mcp.server.context import ServerRequestContext
|
|
16
|
+
from mcp.server.extension import Extension, MethodBinding, ResourceBinding
|
|
17
|
+
from mcp.server.mcpserver import require_client_extension
|
|
18
|
+
from mcp.server.mcpserver.resources import BinaryResource, Resource, TextResource
|
|
19
|
+
from mcp.shared.exceptions import MCPError
|
|
20
|
+
from mcp.types import (
|
|
21
|
+
INVALID_PARAMS,
|
|
22
|
+
METHOD_NOT_FOUND,
|
|
23
|
+
PaginatedRequestParams,
|
|
24
|
+
RequestParams,
|
|
25
|
+
Resource as MCPResource,
|
|
26
|
+
)
|
|
27
|
+
from mcp.types.version import is_version_at_least
|
|
28
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
29
|
+
|
|
30
|
+
SKILLS_EXTENSION_IDENTIFIER = "io.modelcontextprotocol/skills"
|
|
31
|
+
SKILLS_PROTOCOL_VERSION = "2026-07-28"
|
|
32
|
+
SKILL_URI = "skill://slcli/SKILL.md"
|
|
33
|
+
SKILL_ROOT_URI = "skill://slcli"
|
|
34
|
+
DIRECTORY_MIME_TYPE = "inode/directory"
|
|
35
|
+
SKILLS_CACHE_TTL_MS = 300_000
|
|
36
|
+
SKILLS_CACHE_SCOPE = "public"
|
|
37
|
+
MAX_RESOURCES_PER_SKILL = 512
|
|
38
|
+
MAX_SKILL_SIZE = 16 * 1024 * 1024
|
|
39
|
+
|
|
40
|
+
_SKILL_NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
41
|
+
_FRONTMATTER_PATTERN = re.compile(r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.DOTALL)
|
|
42
|
+
_MIME_TYPES = {
|
|
43
|
+
".html": "text/html",
|
|
44
|
+
".json": "application/json",
|
|
45
|
+
".md": "text/markdown",
|
|
46
|
+
".py": "text/x-python",
|
|
47
|
+
".svg": "image/svg+xml",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GetSkillParams(RequestParams):
|
|
52
|
+
"""Parameters for the skills/get request."""
|
|
53
|
+
|
|
54
|
+
uri: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ListSkillsParams(PaginatedRequestParams):
|
|
58
|
+
"""Parameters for the skills/list request."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReadDirectoryParams(PaginatedRequestParams):
|
|
62
|
+
"""Parameters for the resources/directory/read request."""
|
|
63
|
+
|
|
64
|
+
uri: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SkillResourceManifest(BaseModel):
|
|
68
|
+
"""Digest and size metadata for one skill resource."""
|
|
69
|
+
|
|
70
|
+
uri: str
|
|
71
|
+
digest: str
|
|
72
|
+
size: int = Field(ge=0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SkillEntry(BaseModel):
|
|
76
|
+
"""A complete MCP Skills entry for one Agent Skill."""
|
|
77
|
+
|
|
78
|
+
uri: str
|
|
79
|
+
frontmatter: Dict[str, Any]
|
|
80
|
+
resources: List[SkillResourceManifest]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ListSkillsResult(BaseModel):
|
|
84
|
+
"""Result returned by skills/list."""
|
|
85
|
+
|
|
86
|
+
result_type: str = Field(default="complete", alias="resultType")
|
|
87
|
+
skills: List[SkillEntry]
|
|
88
|
+
ttl_ms: int = Field(default=SKILLS_CACHE_TTL_MS, alias="ttlMs", ge=0)
|
|
89
|
+
cache_scope: str = Field(default=SKILLS_CACHE_SCOPE, alias="cacheScope")
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class GetSkillResult(BaseModel):
|
|
95
|
+
"""Result returned by skills/get."""
|
|
96
|
+
|
|
97
|
+
result_type: str = Field(default="complete", alias="resultType")
|
|
98
|
+
skill: SkillEntry
|
|
99
|
+
ttl_ms: int = Field(default=SKILLS_CACHE_TTL_MS, alias="ttlMs", ge=0)
|
|
100
|
+
cache_scope: str = Field(default=SKILLS_CACHE_SCOPE, alias="cacheScope")
|
|
101
|
+
|
|
102
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ReadDirectoryResult(BaseModel):
|
|
106
|
+
"""Result returned by resources/directory/read."""
|
|
107
|
+
|
|
108
|
+
result_type: str = Field(default="complete", alias="resultType")
|
|
109
|
+
resources: List[MCPResource]
|
|
110
|
+
|
|
111
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class SkillFile:
|
|
116
|
+
"""One immutable file in the published skill namespace."""
|
|
117
|
+
|
|
118
|
+
relative_path: str
|
|
119
|
+
uri: str
|
|
120
|
+
content: bytes
|
|
121
|
+
mime_type: str
|
|
122
|
+
resource: Resource
|
|
123
|
+
manifest: SkillResourceManifest
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class SkillCatalog:
|
|
128
|
+
"""The validated, immutable view of one published skill."""
|
|
129
|
+
|
|
130
|
+
entry: SkillEntry
|
|
131
|
+
files: Mapping[str, SkillFile]
|
|
132
|
+
directories: Mapping[str, Tuple[MCPResource, ...]]
|
|
133
|
+
|
|
134
|
+
def resources(self) -> List[ResourceBinding]:
|
|
135
|
+
"""Return all file resources contributed to the MCP server."""
|
|
136
|
+
return [ResourceBinding(resource=file.resource) for file in self.files.values()]
|
|
137
|
+
|
|
138
|
+
def validate_file_uri(self, uri: str) -> SkillFile:
|
|
139
|
+
"""Return a file for a canonical skill URI or raise Invalid params."""
|
|
140
|
+
relative_path = _relative_path_for_uri(uri)
|
|
141
|
+
file = self.files.get(relative_path)
|
|
142
|
+
if file is None:
|
|
143
|
+
raise _invalid_params(f"No skill resource is served at {uri}")
|
|
144
|
+
return file
|
|
145
|
+
|
|
146
|
+
def directory_children(self, uri: str) -> Tuple[MCPResource, ...]:
|
|
147
|
+
"""Return direct children for a canonical directory URI."""
|
|
148
|
+
relative_path = _relative_path_for_uri(uri)
|
|
149
|
+
try:
|
|
150
|
+
return self.directories[uri]
|
|
151
|
+
except KeyError as exc:
|
|
152
|
+
if relative_path in self.files:
|
|
153
|
+
raise _invalid_params(f"{uri} is not a directory resource") from exc
|
|
154
|
+
raise _invalid_params(f"No directory resource is served at {uri}") from exc
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _invalid_params(message: str) -> MCPError:
|
|
158
|
+
"""Create the standard error for an invalid Skills request."""
|
|
159
|
+
return MCPError(code=INVALID_PARAMS, message=message)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _modern_only(
|
|
163
|
+
handler: Callable[[ServerRequestContext[Any, Any], Any], Awaitable[Any]],
|
|
164
|
+
) -> Callable[[ServerRequestContext[Any, Any], Any], Awaitable[Any]]:
|
|
165
|
+
"""Restrict an extension method to the target revision and later known revisions."""
|
|
166
|
+
|
|
167
|
+
async def gated(context: ServerRequestContext[Any, Any], params: Any) -> Any:
|
|
168
|
+
if not is_version_at_least(context.protocol_version, SKILLS_PROTOCOL_VERSION):
|
|
169
|
+
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found")
|
|
170
|
+
require_client_extension(context, SKILLS_EXTENSION_IDENTIFIER)
|
|
171
|
+
return await handler(context, params)
|
|
172
|
+
|
|
173
|
+
return gated
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _skill_root_candidates() -> List[Path]:
|
|
177
|
+
"""Return source and frozen bundled-skill roots."""
|
|
178
|
+
candidates: List[Path] = []
|
|
179
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
180
|
+
if meipass:
|
|
181
|
+
candidates.append(Path(meipass) / "skills" / "slcli")
|
|
182
|
+
if getattr(sys, "frozen", False):
|
|
183
|
+
candidates.append(Path(sys.executable).resolve().parent / "skills" / "slcli")
|
|
184
|
+
candidates.append(Path(__file__).resolve().parent / "skills" / "slcli")
|
|
185
|
+
return candidates
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _find_skill_root() -> Path:
|
|
189
|
+
"""Locate the bundled skill directory."""
|
|
190
|
+
for candidate in _skill_root_candidates():
|
|
191
|
+
if candidate.is_dir():
|
|
192
|
+
return candidate
|
|
193
|
+
raise FileNotFoundError("Bundled slcli skill not found.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _ensure_regular_file(path: Path) -> None:
|
|
197
|
+
"""Reject symlinks and non-regular files in published content."""
|
|
198
|
+
if path.is_symlink():
|
|
199
|
+
raise ValueError(f"Published skill path must not be a symlink: {path}")
|
|
200
|
+
if not path.is_file():
|
|
201
|
+
raise ValueError(f"Published skill path must be a regular file: {path}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _published_paths(root: Path) -> List[Path]:
|
|
205
|
+
"""Return files allowed by the explicit MCP publication policy."""
|
|
206
|
+
if root.is_symlink() or not root.is_dir():
|
|
207
|
+
raise ValueError(f"Skill root must be a real directory: {root}")
|
|
208
|
+
|
|
209
|
+
skill_file = root / "SKILL.md"
|
|
210
|
+
_ensure_regular_file(skill_file)
|
|
211
|
+
paths = [skill_file]
|
|
212
|
+
|
|
213
|
+
references = root / "references"
|
|
214
|
+
if references.is_symlink() or references.exists():
|
|
215
|
+
if references.is_symlink() or not references.is_dir():
|
|
216
|
+
raise ValueError(f"Skill references must be a real directory: {references}")
|
|
217
|
+
for path in sorted(references.rglob("*")):
|
|
218
|
+
if path.is_symlink():
|
|
219
|
+
raise ValueError(f"Published skill path must not be a symlink: {path}")
|
|
220
|
+
relative_parts = path.relative_to(root).parts
|
|
221
|
+
if any(part.startswith(".") or part == "__pycache__" for part in relative_parts):
|
|
222
|
+
continue
|
|
223
|
+
if path.is_dir():
|
|
224
|
+
continue
|
|
225
|
+
if not path.is_file():
|
|
226
|
+
raise ValueError(f"Published skill path must be a regular file: {path}")
|
|
227
|
+
if path.suffix != ".pyc":
|
|
228
|
+
paths.append(path)
|
|
229
|
+
|
|
230
|
+
scripts = root / "scripts"
|
|
231
|
+
if scripts.is_symlink() or scripts.exists():
|
|
232
|
+
if scripts.is_symlink() or not scripts.is_dir():
|
|
233
|
+
raise ValueError(f"Skill scripts must be a real directory: {scripts}")
|
|
234
|
+
for path in sorted(scripts.iterdir()):
|
|
235
|
+
if path.is_symlink():
|
|
236
|
+
raise ValueError(f"Published skill path must not be a symlink: {path}")
|
|
237
|
+
if path.name.startswith(".") or path.name == "__pycache__":
|
|
238
|
+
continue
|
|
239
|
+
if not path.is_file():
|
|
240
|
+
raise ValueError(f"Published skill path must be a regular file: {path}")
|
|
241
|
+
if path.suffix == ".py":
|
|
242
|
+
paths.append(path)
|
|
243
|
+
|
|
244
|
+
return sorted(paths, key=lambda path: path.relative_to(root).as_posix())
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _frontmatter(content: bytes, path: Path) -> Dict[str, Any]:
|
|
248
|
+
"""Parse and validate the complete YAML frontmatter object."""
|
|
249
|
+
try:
|
|
250
|
+
text = content.decode("utf-8")
|
|
251
|
+
except UnicodeDecodeError as exc:
|
|
252
|
+
raise ValueError(f"SKILL.md must be UTF-8: {path}") from exc
|
|
253
|
+
match = _FRONTMATTER_PATTERN.match(text)
|
|
254
|
+
if match is None:
|
|
255
|
+
raise ValueError(f"SKILL.md must begin with YAML frontmatter: {path}")
|
|
256
|
+
parsed = yaml.safe_load(match.group(1))
|
|
257
|
+
if not isinstance(parsed, dict) or not all(isinstance(key, str) for key in parsed):
|
|
258
|
+
raise ValueError(f"SKILL.md frontmatter must be a JSON object: {path}")
|
|
259
|
+
try:
|
|
260
|
+
json.dumps(parsed)
|
|
261
|
+
except (TypeError, ValueError) as exc:
|
|
262
|
+
raise ValueError(f"SKILL.md frontmatter must contain JSON values: {path}") from exc
|
|
263
|
+
name = parsed.get("name")
|
|
264
|
+
description = parsed.get("description")
|
|
265
|
+
if not isinstance(name, str) or not _SKILL_NAME_PATTERN.fullmatch(name) or len(name) > 64:
|
|
266
|
+
raise ValueError(f"SKILL.md has an invalid name: {path}")
|
|
267
|
+
if not isinstance(description, str) or not description.strip():
|
|
268
|
+
raise ValueError(f"SKILL.md must have a non-empty description: {path}")
|
|
269
|
+
return parsed
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _mime_type(path: Path) -> str:
|
|
273
|
+
"""Return the explicit MIME type for a published skill file."""
|
|
274
|
+
try:
|
|
275
|
+
return _MIME_TYPES[path.suffix.lower()]
|
|
276
|
+
except KeyError as exc:
|
|
277
|
+
raise ValueError(f"Unsupported published skill file type: {path}") from exc
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _resource(
|
|
281
|
+
uri: str,
|
|
282
|
+
name: str,
|
|
283
|
+
mime_type: str,
|
|
284
|
+
content: bytes,
|
|
285
|
+
description: Optional[str] = None,
|
|
286
|
+
) -> Resource:
|
|
287
|
+
"""Build a resource whose served content matches the indexed bytes."""
|
|
288
|
+
try:
|
|
289
|
+
text = content.decode("utf-8")
|
|
290
|
+
except UnicodeDecodeError:
|
|
291
|
+
return BinaryResource(
|
|
292
|
+
uri=uri,
|
|
293
|
+
name=name,
|
|
294
|
+
description=description,
|
|
295
|
+
mime_type=mime_type,
|
|
296
|
+
data=content,
|
|
297
|
+
)
|
|
298
|
+
return TextResource(
|
|
299
|
+
uri=uri,
|
|
300
|
+
name=name,
|
|
301
|
+
description=description,
|
|
302
|
+
mime_type=mime_type,
|
|
303
|
+
text=text,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _uri_for(relative_path: str) -> str:
|
|
308
|
+
"""Return the canonical URI for a skill-relative POSIX path."""
|
|
309
|
+
return f"{SKILL_ROOT_URI}/{relative_path}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _relative_path_for_uri(uri: str) -> str:
|
|
313
|
+
"""Validate a skill URI and return its canonical relative path."""
|
|
314
|
+
try:
|
|
315
|
+
parsed = urlsplit(uri)
|
|
316
|
+
except ValueError as exc:
|
|
317
|
+
raise _invalid_params(f"Invalid skill URI: {uri}") from exc
|
|
318
|
+
decoded_path = unquote(parsed.path)
|
|
319
|
+
if (
|
|
320
|
+
parsed.scheme == "skill"
|
|
321
|
+
and parsed.netloc == "slcli"
|
|
322
|
+
and not parsed.query
|
|
323
|
+
and not parsed.fragment
|
|
324
|
+
and decoded_path == ""
|
|
325
|
+
and uri == SKILL_ROOT_URI
|
|
326
|
+
):
|
|
327
|
+
return ""
|
|
328
|
+
if (
|
|
329
|
+
parsed.scheme != "skill"
|
|
330
|
+
or parsed.netloc != "slcli"
|
|
331
|
+
or parsed.query
|
|
332
|
+
or parsed.fragment
|
|
333
|
+
or "\\" in decoded_path
|
|
334
|
+
or "\x00" in decoded_path
|
|
335
|
+
or any(part in {"", ".", ".."} for part in decoded_path.lstrip("/").split("/"))
|
|
336
|
+
or not decoded_path.startswith("/")
|
|
337
|
+
):
|
|
338
|
+
raise _invalid_params(f"Invalid skill URI: {uri}")
|
|
339
|
+
relative_path = decoded_path.lstrip("/")
|
|
340
|
+
canonical = _uri_for(relative_path) if relative_path else SKILL_ROOT_URI
|
|
341
|
+
if uri != canonical:
|
|
342
|
+
raise _invalid_params(f"Invalid skill URI: {uri}")
|
|
343
|
+
return relative_path
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def build_skill_catalog(root: Path) -> SkillCatalog:
|
|
347
|
+
"""Build and fully validate the static catalog for the bundled skill."""
|
|
348
|
+
paths = _published_paths(root)
|
|
349
|
+
if len(paths) > MAX_RESOURCES_PER_SKILL:
|
|
350
|
+
raise ValueError("Bundled slcli skill exceeds the resource limit")
|
|
351
|
+
|
|
352
|
+
contents = {path: path.read_bytes() for path in paths}
|
|
353
|
+
skill_content = contents[root / "SKILL.md"]
|
|
354
|
+
frontmatter = _frontmatter(skill_content, root / "SKILL.md")
|
|
355
|
+
if frontmatter["name"] != root.name:
|
|
356
|
+
raise ValueError("SKILL.md name must match its skill directory")
|
|
357
|
+
|
|
358
|
+
total_size = sum(len(content) for content in contents.values())
|
|
359
|
+
if total_size > MAX_SKILL_SIZE:
|
|
360
|
+
raise ValueError("Bundled slcli skill exceeds the total size limit")
|
|
361
|
+
|
|
362
|
+
files: Dict[str, SkillFile] = {}
|
|
363
|
+
for path, content in contents.items():
|
|
364
|
+
relative_path = path.relative_to(root).as_posix()
|
|
365
|
+
uri = _uri_for(relative_path)
|
|
366
|
+
mime_type = _mime_type(path)
|
|
367
|
+
manifest = SkillResourceManifest(
|
|
368
|
+
uri=uri,
|
|
369
|
+
digest=f"sha256:{hashlib.sha256(content).hexdigest()}",
|
|
370
|
+
size=len(content),
|
|
371
|
+
)
|
|
372
|
+
is_skill_file = relative_path == "SKILL.md"
|
|
373
|
+
files[relative_path] = SkillFile(
|
|
374
|
+
relative_path=relative_path,
|
|
375
|
+
uri=uri,
|
|
376
|
+
content=content,
|
|
377
|
+
mime_type=mime_type,
|
|
378
|
+
resource=_resource(
|
|
379
|
+
uri,
|
|
380
|
+
frontmatter["name"] if is_skill_file else path.name,
|
|
381
|
+
mime_type,
|
|
382
|
+
content,
|
|
383
|
+
frontmatter["description"] if is_skill_file else None,
|
|
384
|
+
),
|
|
385
|
+
manifest=manifest,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
directories: Dict[str, List[MCPResource]] = {SKILL_ROOT_URI: []}
|
|
389
|
+
for relative_path in files:
|
|
390
|
+
parts = relative_path.split("/")
|
|
391
|
+
for index in range(1, len(parts)):
|
|
392
|
+
directory_path = "/".join(parts[:index])
|
|
393
|
+
directories.setdefault(_uri_for(directory_path), [])
|
|
394
|
+
|
|
395
|
+
for directory_uri in list(directories):
|
|
396
|
+
relative_directory = _relative_path_for_uri(directory_uri)
|
|
397
|
+
children: Dict[str, MCPResource] = {}
|
|
398
|
+
for relative_path, file in files.items():
|
|
399
|
+
path = Path(relative_path)
|
|
400
|
+
parent = (
|
|
401
|
+
SKILL_ROOT_URI if path.parent == Path(".") else _uri_for(path.parent.as_posix())
|
|
402
|
+
)
|
|
403
|
+
if parent == directory_uri:
|
|
404
|
+
children[path.name] = MCPResource(
|
|
405
|
+
uri=file.uri,
|
|
406
|
+
name=frontmatter["name"] if relative_path == "SKILL.md" else path.name,
|
|
407
|
+
description=(
|
|
408
|
+
frontmatter["description"] if relative_path == "SKILL.md" else None
|
|
409
|
+
),
|
|
410
|
+
mime_type=file.mime_type,
|
|
411
|
+
)
|
|
412
|
+
elif relative_directory and relative_path.startswith(f"{relative_directory}/"):
|
|
413
|
+
child_name = relative_path[len(relative_directory) + 1 :].split("/", 1)[0]
|
|
414
|
+
child_path = f"{relative_directory}/{child_name}"
|
|
415
|
+
children.setdefault(
|
|
416
|
+
child_name,
|
|
417
|
+
MCPResource(
|
|
418
|
+
uri=_uri_for(child_path), name=child_name, mime_type=DIRECTORY_MIME_TYPE
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
elif not relative_directory and "/" in relative_path:
|
|
422
|
+
child_name = relative_path.split("/", 1)[0]
|
|
423
|
+
children.setdefault(
|
|
424
|
+
child_name,
|
|
425
|
+
MCPResource(
|
|
426
|
+
uri=_uri_for(child_name), name=child_name, mime_type=DIRECTORY_MIME_TYPE
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
directories[directory_uri] = list(children.values())
|
|
430
|
+
directories[directory_uri].sort(key=lambda resource: resource.name or "")
|
|
431
|
+
|
|
432
|
+
manifests = [files[path].manifest for path in sorted(files)]
|
|
433
|
+
entry = SkillEntry(uri=SKILL_URI, frontmatter=frontmatter, resources=manifests)
|
|
434
|
+
return SkillCatalog(
|
|
435
|
+
entry=entry,
|
|
436
|
+
files=files,
|
|
437
|
+
directories={key: tuple(value) for key, value in directories.items()},
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class SlcliSkillsExtension(Extension):
|
|
442
|
+
"""Serve the packaged slcli Agent Skill through the MCP Skills extension."""
|
|
443
|
+
|
|
444
|
+
identifier = SKILLS_EXTENSION_IDENTIFIER
|
|
445
|
+
|
|
446
|
+
def __init__(self, root: Optional[Path] = None) -> None:
|
|
447
|
+
"""Build and validate the static catalog from the bundled skill root."""
|
|
448
|
+
self.catalog = build_skill_catalog(root or _find_skill_root())
|
|
449
|
+
|
|
450
|
+
def settings(self) -> Dict[str, Any]:
|
|
451
|
+
"""Advertise optional directory reads."""
|
|
452
|
+
return {"directoryRead": True}
|
|
453
|
+
|
|
454
|
+
def resources(self) -> List[ResourceBinding]:
|
|
455
|
+
"""Contribute every published skill file as a normal MCP resource."""
|
|
456
|
+
return self.catalog.resources()
|
|
457
|
+
|
|
458
|
+
def methods(self) -> List[MethodBinding]:
|
|
459
|
+
"""Return the Skills extension request handlers."""
|
|
460
|
+
return [
|
|
461
|
+
MethodBinding("skills/list", ListSkillsParams, _modern_only(self._list_skills)),
|
|
462
|
+
MethodBinding("skills/get", GetSkillParams, _modern_only(self._get_skill)),
|
|
463
|
+
MethodBinding(
|
|
464
|
+
"resources/directory/read", ReadDirectoryParams, _modern_only(self._read_directory)
|
|
465
|
+
),
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
async def _list_skills(
|
|
469
|
+
self, _: ServerRequestContext[Any, Any], params: ListSkillsParams
|
|
470
|
+
) -> ListSkillsResult:
|
|
471
|
+
"""Return the complete static skill catalog."""
|
|
472
|
+
if params.cursor is not None:
|
|
473
|
+
raise _invalid_params("The static slcli skill catalog does not support pagination")
|
|
474
|
+
return ListSkillsResult(skills=[self.catalog.entry])
|
|
475
|
+
|
|
476
|
+
async def _get_skill(
|
|
477
|
+
self, _: ServerRequestContext[Any, Any], params: GetSkillParams
|
|
478
|
+
) -> GetSkillResult:
|
|
479
|
+
"""Return the catalog entry for the requested skill URI."""
|
|
480
|
+
if params.uri != SKILL_URI:
|
|
481
|
+
raise _invalid_params(f"No skill is served at {params.uri}")
|
|
482
|
+
return GetSkillResult(skill=self.catalog.entry)
|
|
483
|
+
|
|
484
|
+
async def _read_directory(
|
|
485
|
+
self, _: ServerRequestContext[Any, Any], params: ReadDirectoryParams
|
|
486
|
+
) -> ReadDirectoryResult:
|
|
487
|
+
"""Return direct children of a published skill directory."""
|
|
488
|
+
if params.cursor is not None:
|
|
489
|
+
raise _invalid_params("The static slcli skill directories do not support pagination")
|
|
490
|
+
return ReadDirectoryResult(resources=list(self.catalog.directory_children(params.uri)))
|