@skilljack/mcp 0.7.0 → 0.7.1

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,269 @@
1
+ /**
2
+ * Skill Display MCP App - Vanilla JS implementation
3
+ */
4
+ import { App, applyDocumentTheme, applyHostStyleVariables, applyHostFonts, } from "@modelcontextprotocol/ext-apps";
5
+ // State
6
+ let skills = [];
7
+ let searchQuery = "";
8
+ let app = null;
9
+ // DOM Elements
10
+ const skillList = document.getElementById("skill-list");
11
+ const stats = document.getElementById("stats");
12
+ const searchInput = document.getElementById("search-input");
13
+ const toast = document.getElementById("toast");
14
+ // Handle host context changes
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ function handleHostContextChanged(ctx) {
17
+ if (ctx.theme) {
18
+ applyDocumentTheme(ctx.theme);
19
+ }
20
+ if (ctx.styles?.variables) {
21
+ applyHostStyleVariables(ctx.styles.variables);
22
+ }
23
+ if (ctx.styles?.css?.fonts) {
24
+ applyHostFonts(ctx.styles.css.fonts);
25
+ }
26
+ // Handle safe area insets for mobile/notched devices
27
+ if (ctx.safeAreaInsets) {
28
+ const { top, right, bottom, left } = ctx.safeAreaInsets;
29
+ document.body.style.paddingTop = `${top + 16}px`;
30
+ document.body.style.paddingRight = `${right + 16}px`;
31
+ document.body.style.paddingBottom = `${bottom + 16}px`;
32
+ document.body.style.paddingLeft = `${left + 16}px`;
33
+ }
34
+ }
35
+ // Update state from tool result
36
+ function updateState(data) {
37
+ if (data.skills) {
38
+ skills = data.skills;
39
+ }
40
+ render();
41
+ }
42
+ // Get filtered skills based on search query
43
+ function getFilteredSkills() {
44
+ if (!searchQuery) {
45
+ return skills;
46
+ }
47
+ const query = searchQuery.toLowerCase();
48
+ return skills.filter((skill) => skill.name.toLowerCase().includes(query) ||
49
+ skill.description.toLowerCase().includes(query));
50
+ }
51
+ // Render the UI
52
+ function render() {
53
+ renderStats();
54
+ renderSkills();
55
+ }
56
+ function renderStats() {
57
+ const filtered = getFilteredSkills();
58
+ if (searchQuery) {
59
+ stats.textContent = `${filtered.length} of ${skills.length} skills`;
60
+ }
61
+ else {
62
+ stats.textContent = `${skills.length} skill${skills.length !== 1 ? "s" : ""} available`;
63
+ }
64
+ }
65
+ function renderSkills() {
66
+ const filtered = getFilteredSkills();
67
+ if (skills.length === 0) {
68
+ skillList.innerHTML = `
69
+ <div class="empty-state">
70
+ <p>No skills available.</p>
71
+ <p>Configure skill directories using the skill-config tool.</p>
72
+ </div>
73
+ `;
74
+ return;
75
+ }
76
+ if (filtered.length === 0) {
77
+ skillList.innerHTML = `
78
+ <div class="empty-state">
79
+ <p>No skills match your search.</p>
80
+ </div>
81
+ `;
82
+ return;
83
+ }
84
+ skillList.innerHTML = filtered
85
+ .map((skill) => {
86
+ const isCustomized = skill.isAssistantOverridden || skill.isUserOverridden;
87
+ // Build source badge based on type
88
+ let sourceBadge;
89
+ if (skill.sourceType === "github") {
90
+ sourceBadge = `<span class="source-badge github" title="From GitHub: ${escapeHtml(skill.sourceDisplayName)}">
91
+ <svg class="source-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
92
+ <path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
93
+ </svg>
94
+ ${escapeHtml(skill.sourceDisplayName)}
95
+ </span>`;
96
+ }
97
+ else if (skill.sourceType === "bundled") {
98
+ sourceBadge = `<span class="source-badge bundled" title="Bundled with server">
99
+ <svg class="source-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
100
+ <path fill="currentColor" d="M8.878.392a1.75 1.75 0 0 0-1.756 0l-5.25 3.045A1.75 1.75 0 0 0 1 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 0 0 1.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392ZM8 3.5a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V4.25A.75.75 0 0 1 8 3.5Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
101
+ </svg>
102
+ Bundled
103
+ </span>`;
104
+ }
105
+ else {
106
+ sourceBadge = `<span class="source-badge local" title="Local skill directory">
107
+ <svg class="source-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
108
+ <path fill="currentColor" d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z"/>
109
+ </svg>
110
+ Local
111
+ </span>`;
112
+ }
113
+ return `
114
+ <div class="skill-card" data-skill="${escapeHtml(skill.name)}">
115
+ <div class="skill-header">
116
+ <span class="skill-name">${escapeHtml(skill.name)}</span>
117
+ <div class="skill-badges">
118
+ ${sourceBadge}
119
+ ${isCustomized ? '<span class="customized-badge">Customized</span>' : ""}
120
+ </div>
121
+ </div>
122
+ <p class="skill-description">${escapeHtml(skill.description)}</p>
123
+ ${skill.sourceType !== "bundled" ? `<div class="skill-path">${escapeHtml(skill.path)}</div>` : ""}
124
+ <div class="skill-controls">
125
+ <div class="toggle-group">
126
+ <span class="toggle-label ${skill.isAssistantOverridden ? "overridden" : ""}">Assistant</span>
127
+ <div
128
+ class="toggle-switch ${skill.assistantInvocable ? "active" : ""}"
129
+ data-skill="${escapeHtml(skill.name)}"
130
+ data-setting="assistant"
131
+ data-value="${skill.assistantInvocable}"
132
+ title="${skill.assistantInvocable ? "Model can auto-invoke this skill" : "Model cannot auto-invoke this skill"}"
133
+ ></div>
134
+ </div>
135
+ <div class="toggle-group">
136
+ <span class="toggle-label ${skill.isUserOverridden ? "overridden" : ""}">User</span>
137
+ <div
138
+ class="toggle-switch ${skill.userInvocable ? "active" : ""}"
139
+ data-skill="${escapeHtml(skill.name)}"
140
+ data-setting="user"
141
+ data-value="${skill.userInvocable}"
142
+ title="${skill.userInvocable ? "Appears in prompts menu" : "Hidden from prompts menu"}"
143
+ ></div>
144
+ </div>
145
+ <button
146
+ class="reset-btn"
147
+ data-skill="${escapeHtml(skill.name)}"
148
+ ${!isCustomized ? "disabled" : ""}
149
+ title="Reset to frontmatter defaults"
150
+ >Reset</button>
151
+ </div>
152
+ </div>
153
+ `;
154
+ })
155
+ .join("");
156
+ // Add click handlers for toggle switches
157
+ skillList.querySelectorAll(".toggle-switch").forEach((toggle) => {
158
+ toggle.addEventListener("click", () => {
159
+ const skillName = toggle.dataset.skill;
160
+ const setting = toggle.dataset.setting;
161
+ const currentValue = toggle.dataset.value === "true";
162
+ if (skillName && setting) {
163
+ updateInvocation(skillName, setting, !currentValue);
164
+ }
165
+ });
166
+ });
167
+ // Add click handlers for reset buttons
168
+ skillList.querySelectorAll(".reset-btn").forEach((btn) => {
169
+ btn.addEventListener("click", () => {
170
+ const skillName = btn.dataset.skill;
171
+ if (skillName) {
172
+ resetOverride(skillName);
173
+ }
174
+ });
175
+ });
176
+ }
177
+ // Update invocation setting
178
+ async function updateInvocation(skillName, setting, value) {
179
+ try {
180
+ const result = await app.callServerTool({
181
+ name: "skill-display-update-invocation",
182
+ arguments: { skillName, setting, value },
183
+ });
184
+ console.log("Update result:", result);
185
+ const structured = result.structuredContent;
186
+ if (structured?.success) {
187
+ updateState(structured);
188
+ showToast(`${skillName}: ${setting} = ${value ? "on" : "off"}`, "success");
189
+ }
190
+ else {
191
+ showToast(structured?.error || "Failed to update", "error");
192
+ }
193
+ }
194
+ catch (error) {
195
+ console.error("Update invocation error:", error);
196
+ showToast(error.message || "Failed to update", "error");
197
+ }
198
+ }
199
+ // Reset override
200
+ async function resetOverride(skillName) {
201
+ try {
202
+ const result = await app.callServerTool({
203
+ name: "skill-display-reset-override",
204
+ arguments: { skillName },
205
+ });
206
+ console.log("Reset result:", result);
207
+ const structured = result.structuredContent;
208
+ if (structured?.success) {
209
+ updateState(structured);
210
+ showToast(`${skillName} reset to defaults`, "success");
211
+ }
212
+ else {
213
+ showToast(structured?.error || "Failed to reset", "error");
214
+ }
215
+ }
216
+ catch (error) {
217
+ console.error("Reset override error:", error);
218
+ showToast(error.message || "Failed to reset", "error");
219
+ }
220
+ }
221
+ // Toast
222
+ function showToast(message, type = "success") {
223
+ toast.textContent = message;
224
+ toast.className = `toast ${type} visible`;
225
+ setTimeout(() => {
226
+ toast.classList.remove("visible");
227
+ }, 3000);
228
+ }
229
+ // Utilities
230
+ function escapeHtml(str) {
231
+ if (!str)
232
+ return "";
233
+ return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c);
234
+ }
235
+ // Set up event listeners
236
+ searchInput.addEventListener("input", () => {
237
+ searchQuery = searchInput.value.trim();
238
+ render();
239
+ });
240
+ // 1. Create app instance
241
+ app = new App({ name: "Skill Display", version: "1.0.0" });
242
+ // 2. Register handlers BEFORE connecting
243
+ app.onteardown = async () => {
244
+ console.info("App is being torn down");
245
+ return {};
246
+ };
247
+ app.ontoolinput = (params) => {
248
+ console.info("Received tool input:", params);
249
+ };
250
+ app.ontoolresult = (result) => {
251
+ console.info("Received tool result:", result);
252
+ if (result.structuredContent) {
253
+ updateState(result.structuredContent);
254
+ }
255
+ };
256
+ app.ontoolcancelled = (params) => {
257
+ console.info("Tool call cancelled:", params.reason);
258
+ };
259
+ app.onerror = console.error;
260
+ app.onhostcontextchanged = handleHostContextChanged;
261
+ // 3. Connect to host
262
+ app.connect().then(() => {
263
+ console.info("Connected to host");
264
+ // Apply initial host context
265
+ const ctx = app.getHostContext();
266
+ if (ctx) {
267
+ handleHostContextChanged(ctx);
268
+ }
269
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilljack/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "MCP server that discovers and serves Agent Skills. I know kung fu.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",