@steercards/agent-tools 0.0.0-stage → 0.1.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.
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SECURITY.md +28 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +217 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +58 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/install.d.ts +22 -0
- package/dist/install.js +107 -0
- package/dist/package.d.ts +5 -0
- package/dist/package.js +5 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +214 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +121 -0
- package/dist/setup.d.ts +34 -0
- package/dist/setup.js +458 -0
- package/docs/releases.md +51 -0
- package/package.json +64 -4
- package/skills/steercards/SKILL.md +44 -0
- package/skills/steercards/references/cards.md +34 -0
- package/skills/steercards/references/claims-runs.md +33 -0
- package/skills/steercards/references/collaboration.md +29 -0
- package/skills/steercards/references/context.md +21 -0
- package/skills/steercards/references/discovery.md +32 -0
- package/skills/steercards/references/files.md +32 -0
- package/skills/steercards/references/profiles.md +29 -0
package/dist/install.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFile, readdir, mkdir, lstat, rename, rm, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { zipSync } from 'fflate';
|
|
5
|
+
import { validateMcpUrl } from './config.js';
|
|
6
|
+
import { packageRoot, packageInfo } from './package.js';
|
|
7
|
+
export { packageRoot, packageInfo } from './package.js';
|
|
8
|
+
export const skillRoot = join(packageRoot, 'skills', 'steercards');
|
|
9
|
+
export const clients = ['claude-code', 'codex', 'codex-app', 'codex-cli', 'cursor', 'claude-desktop'];
|
|
10
|
+
export function clientConfig(client, options = {}) {
|
|
11
|
+
if (!clients.includes(client))
|
|
12
|
+
throw new Error(`Unknown client. Choose ${clients.join(', ')}.`);
|
|
13
|
+
const mode = options.mode ?? 'compact';
|
|
14
|
+
if (!['compact', 'full'].includes(mode))
|
|
15
|
+
throw new Error('Mode must be compact or full.');
|
|
16
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
17
|
+
const args = options.local ? [join(packageRoot, 'dist', 'cli.js')] : ['-y', `${packageInfo.name}@${packageInfo.version}`];
|
|
18
|
+
args.push('serve', '--cwd', cwd, '--mode', mode);
|
|
19
|
+
if (options.envFile)
|
|
20
|
+
args.push('--env-file', resolve(cwd, options.envFile));
|
|
21
|
+
if (options.url) {
|
|
22
|
+
args.push('--url', validateMcpUrl(options.url));
|
|
23
|
+
}
|
|
24
|
+
const command = options.local ? process.execPath : 'npx';
|
|
25
|
+
if (client.startsWith('codex'))
|
|
26
|
+
return `[mcp_servers.steercards]\ncommand = ${JSON.stringify(command)}\nargs = ${JSON.stringify(args)}\n`;
|
|
27
|
+
return JSON.stringify({ mcpServers: { steercards: { ...(client === 'cursor' ? { type: 'stdio' } : {}), command, args } } }, null, 2) + '\n';
|
|
28
|
+
}
|
|
29
|
+
async function skillFiles(root, prefix = '') {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const entry of await readdir(join(root, prefix), { withFileTypes: true })) {
|
|
32
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
33
|
+
if (entry.isDirectory())
|
|
34
|
+
Object.assign(result, await skillFiles(root, path));
|
|
35
|
+
else if (entry.isFile())
|
|
36
|
+
result[path] = await readFile(join(root, path));
|
|
37
|
+
else
|
|
38
|
+
throw new Error('Skill source must contain only regular files and directories.');
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
async function assertNoSymlink(path) {
|
|
43
|
+
let current = resolve(path);
|
|
44
|
+
while (true) {
|
|
45
|
+
try {
|
|
46
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
47
|
+
throw new Error('Refusing to install through a symbolic link.');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== 'ENOENT')
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current)
|
|
55
|
+
break;
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function installSkill(options) {
|
|
60
|
+
const dirs = { 'claude-code': '.claude', codex: '.agents', 'codex-cli': '.agents', 'codex-app': '.agents', cursor: '.cursor' };
|
|
61
|
+
const dir = dirs[options.client];
|
|
62
|
+
if (!dir)
|
|
63
|
+
throw new Error('For Claude Desktop use skill export and upload the ZIP, or read steercards_get_guide through MCP.');
|
|
64
|
+
const base = options.global ? (options.home ?? homedir()) : resolve(options.cwd ?? process.cwd());
|
|
65
|
+
const target = join(base, dir, 'skills', 'steercards');
|
|
66
|
+
await assertNoSymlink(target);
|
|
67
|
+
const files = await skillFiles(options.source ?? skillRoot);
|
|
68
|
+
try {
|
|
69
|
+
const existing = await skillFiles(target);
|
|
70
|
+
if (Object.keys(existing).length === Object.keys(files).length && Object.entries(files).every(([name, data]) => existing[name] && Buffer.from(data).equals(existing[name])))
|
|
71
|
+
return { path: target, status: 'unchanged' };
|
|
72
|
+
throw new Error(`Existing skill differs at ${target}. Review and remove or move that directory before reinstalling.`);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code !== 'ENOENT')
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
await mkdir(dirname(target), { recursive: true });
|
|
79
|
+
const stage = await mkdtemp(join(dirname(target), '.steercards-'));
|
|
80
|
+
try {
|
|
81
|
+
for (const [name, data] of Object.entries(files)) {
|
|
82
|
+
await mkdir(dirname(join(stage, name)), { recursive: true });
|
|
83
|
+
await writeFile(join(stage, name), data, { flag: 'wx' });
|
|
84
|
+
}
|
|
85
|
+
// Reject races with existing installs, including an empty directory.
|
|
86
|
+
try {
|
|
87
|
+
await lstat(target);
|
|
88
|
+
throw new Error('Skill destination was created during installation; retry after reviewing it.');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code !== 'ENOENT')
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
await rename(stage, target);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await rm(stage, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
return { path: target, status: 'installed' };
|
|
100
|
+
}
|
|
101
|
+
export async function exportSkill(output, source = skillRoot) {
|
|
102
|
+
const files = await skillFiles(source);
|
|
103
|
+
const entries = Object.fromEntries(Object.entries(files).map(([name, bytes]) => [`steercards/${name}`, bytes]));
|
|
104
|
+
const target = resolve(output);
|
|
105
|
+
await writeFile(target, zipSync(entries), { flag: 'wx' });
|
|
106
|
+
return target;
|
|
107
|
+
}
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
5
|
+
export const packageInfo = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CallToolResult, type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { type Config } from './config.js';
|
|
3
|
+
export type ToolSummary = Pick<Tool, 'name' | 'title' | 'annotations'> & {
|
|
4
|
+
description?: string;
|
|
5
|
+
};
|
|
6
|
+
/** One authenticated upstream client. Calls are never automatically retried. */
|
|
7
|
+
export declare class Runtime {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private client?;
|
|
10
|
+
private transport?;
|
|
11
|
+
private connecting?;
|
|
12
|
+
private catalog?;
|
|
13
|
+
private listing?;
|
|
14
|
+
private closed;
|
|
15
|
+
constructor(config: Config);
|
|
16
|
+
/** Redact credentials if a transport or upstream RPC error echoes them. */
|
|
17
|
+
private safeError;
|
|
18
|
+
private connection;
|
|
19
|
+
listTools({ refresh }?: {
|
|
20
|
+
refresh?: boolean;
|
|
21
|
+
}): Promise<Tool[]>;
|
|
22
|
+
searchTools(query: string, limit?: number): Promise<ToolSummary[]>;
|
|
23
|
+
describeTool(name: string): Promise<Tool>;
|
|
24
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<CallToolResult>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
|
+
import { CallToolResultSchema, ListToolsResultSchema, ToolListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { validateMcpUrl } from './config.js';
|
|
5
|
+
import { packageInfo } from './package.js';
|
|
6
|
+
/** Extract only a small, machine-readable code; arbitrary error prose stays private. */
|
|
7
|
+
async function httpErrorCode(response, agentKey) {
|
|
8
|
+
if (!response.headers.get('content-type')?.toLowerCase().includes('application/json') || !response.body) {
|
|
9
|
+
await response.body?.cancel();
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const reader = response.body.getReader();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let length = 0;
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done)
|
|
19
|
+
break;
|
|
20
|
+
length += value.length;
|
|
21
|
+
if (length > 16 * 1024)
|
|
22
|
+
return undefined;
|
|
23
|
+
chunks.push(value);
|
|
24
|
+
}
|
|
25
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
26
|
+
const code = parsed?.error?.code;
|
|
27
|
+
if (typeof code === 'string' && /^[A-Z][A-Z0-9_]{1,79}$/.test(code)
|
|
28
|
+
&& !code.includes(agentKey) && !agentKey.includes(code))
|
|
29
|
+
return code;
|
|
30
|
+
}
|
|
31
|
+
catch { /* A malformed or interrupted diagnostic body is not itself an error. */ }
|
|
32
|
+
finally {
|
|
33
|
+
await reader.cancel().catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/** One authenticated upstream client. Calls are never automatically retried. */
|
|
38
|
+
export class Runtime {
|
|
39
|
+
config;
|
|
40
|
+
client;
|
|
41
|
+
transport;
|
|
42
|
+
connecting;
|
|
43
|
+
catalog;
|
|
44
|
+
listing;
|
|
45
|
+
closed = false;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = { ...config, url: validateMcpUrl(config.url) };
|
|
48
|
+
if (!config.agentKey || /\s/.test(config.agentKey))
|
|
49
|
+
throw new Error('STEERCARDS_AGENT_KEY is invalid.');
|
|
50
|
+
}
|
|
51
|
+
/** Redact credentials if a transport or upstream RPC error echoes them. */
|
|
52
|
+
safeError(error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : 'Steercards MCP request failed.';
|
|
54
|
+
return new Error(message.split(this.config.agentKey).join('[redacted]'));
|
|
55
|
+
}
|
|
56
|
+
async connection() {
|
|
57
|
+
if (this.closed)
|
|
58
|
+
throw new Error('Steercards runtime is closed.');
|
|
59
|
+
if (this.client)
|
|
60
|
+
return this.client;
|
|
61
|
+
if (!this.connecting) {
|
|
62
|
+
this.connecting = (async () => {
|
|
63
|
+
const client = new Client({ name: packageInfo.name, version: packageInfo.version });
|
|
64
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { this.catalog = undefined; });
|
|
65
|
+
const endpoint = this.config.url;
|
|
66
|
+
const transport = new StreamableHTTPClientTransport(new URL(endpoint), {
|
|
67
|
+
requestInit: { headers: { Authorization: `Bearer ${this.config.agentKey}` }, redirect: 'error' },
|
|
68
|
+
reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1000, maxReconnectionDelay: 1000, reconnectionDelayGrowFactor: 1 },
|
|
69
|
+
fetch: async (input, init) => {
|
|
70
|
+
// Every request stays on the explicitly selected endpoint. Even same-origin
|
|
71
|
+
// redirects are refused so bearer credentials cannot move to another route.
|
|
72
|
+
const target = input instanceof Request ? input.url : input.toString();
|
|
73
|
+
if (target !== endpoint)
|
|
74
|
+
throw new Error('Refused a request outside the configured MCP endpoint.');
|
|
75
|
+
const response = await fetch(input, { ...init, redirect: 'error' });
|
|
76
|
+
if (!response.ok && response.status !== 405) {
|
|
77
|
+
const code = await httpErrorCode(response, this.config.agentKey);
|
|
78
|
+
const retry = response.headers.get('retry-after');
|
|
79
|
+
const retryHint = retry && /^\d{1,8}$/.test(retry) ? ` Retry-After: ${retry} seconds.` : '';
|
|
80
|
+
const hint = code === 'AGENT_AUDIT_UNAVAILABLE'
|
|
81
|
+
? ' The mutation may have committed before audit recording failed. Inspect current state before retrying.'
|
|
82
|
+
: response.status === 429 ? ' Rate limit reached; inspect state before retrying an uncertain mutation.' : '';
|
|
83
|
+
// SDK errors include HTTP response bodies. Supply a safe diagnostic body
|
|
84
|
+
// instead of propagating arbitrary proxy/error pages containing secrets.
|
|
85
|
+
return new Response(`HTTP ${response.status}${code ? ` (${code})` : ''}.${retryHint}${hint}`, {
|
|
86
|
+
status: response.status, headers: response.headers,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return response;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
this.transport = transport;
|
|
93
|
+
try {
|
|
94
|
+
await client.connect(transport);
|
|
95
|
+
if (this.closed) {
|
|
96
|
+
await client.close();
|
|
97
|
+
throw new Error('Steercards runtime is closed.');
|
|
98
|
+
}
|
|
99
|
+
this.client = client;
|
|
100
|
+
return client;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
await client.close().catch(() => { });
|
|
104
|
+
if (this.transport === transport)
|
|
105
|
+
this.transport = undefined;
|
|
106
|
+
throw this.safeError(error);
|
|
107
|
+
}
|
|
108
|
+
})().finally(() => { this.connecting = undefined; });
|
|
109
|
+
}
|
|
110
|
+
return this.connecting;
|
|
111
|
+
}
|
|
112
|
+
async listTools({ refresh = false } = {}) {
|
|
113
|
+
if (this.closed)
|
|
114
|
+
throw new Error('Steercards runtime is closed.');
|
|
115
|
+
if (this.catalog && !refresh)
|
|
116
|
+
return structuredClone(this.catalog);
|
|
117
|
+
if (!this.listing) {
|
|
118
|
+
this.listing = (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const client = await this.connection();
|
|
121
|
+
const tools = [];
|
|
122
|
+
const cursors = new Set();
|
|
123
|
+
const names = new Set();
|
|
124
|
+
let cursor;
|
|
125
|
+
do {
|
|
126
|
+
// Avoid the SDK convenience method's output-schema compilation: a
|
|
127
|
+
// proxy must accept and pass through the server's JSON Schemas.
|
|
128
|
+
const page = await client.request({ method: 'tools/list', params: cursor ? { cursor } : {} }, ListToolsResultSchema);
|
|
129
|
+
for (const tool of page.tools) {
|
|
130
|
+
if (names.has(tool.name))
|
|
131
|
+
throw new Error('The upstream tool catalog contains duplicate tool names.');
|
|
132
|
+
names.add(tool.name);
|
|
133
|
+
tools.push(tool);
|
|
134
|
+
}
|
|
135
|
+
cursor = page.nextCursor;
|
|
136
|
+
if (cursor) {
|
|
137
|
+
if (cursors.has(cursor))
|
|
138
|
+
throw new Error('The upstream tool catalog repeated a pagination cursor.');
|
|
139
|
+
cursors.add(cursor);
|
|
140
|
+
}
|
|
141
|
+
} while (cursor);
|
|
142
|
+
this.catalog = tools;
|
|
143
|
+
return tools;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw this.safeError(error);
|
|
147
|
+
}
|
|
148
|
+
})().finally(() => { this.listing = undefined; });
|
|
149
|
+
}
|
|
150
|
+
return structuredClone(await this.listing);
|
|
151
|
+
}
|
|
152
|
+
async searchTools(query, limit = 10) {
|
|
153
|
+
if (typeof query !== 'string')
|
|
154
|
+
throw new Error('query must be a string.');
|
|
155
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
156
|
+
throw new Error('limit must be an integer from 1 to 100.');
|
|
157
|
+
const terms = query.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
158
|
+
// Stateless upstream servers cannot deliver catalog-change notifications.
|
|
159
|
+
// An explicit discovery search therefore refreshes the current catalog.
|
|
160
|
+
return (await this.listTools({ refresh: true })).map(tool => {
|
|
161
|
+
const name = tool.name.toLocaleLowerCase();
|
|
162
|
+
const text = `${name} ${tool.title ?? ''} ${tool.description ?? ''}`.toLocaleLowerCase();
|
|
163
|
+
const score = terms.every(term => text.includes(term))
|
|
164
|
+
? terms.reduce((sum, term) => sum + (name === term ? 100 : name.includes(term) ? 10 : 1), 0) : -1;
|
|
165
|
+
return { tool, score };
|
|
166
|
+
}).filter(item => item.score >= 0)
|
|
167
|
+
.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name))
|
|
168
|
+
.slice(0, limit).map(({ tool }) => ({
|
|
169
|
+
name: tool.name,
|
|
170
|
+
...(tool.title ? { title: tool.title } : {}),
|
|
171
|
+
...(tool.description ? { description: tool.description.length > 360 ? `${tool.description.slice(0, 357)}...` : tool.description } : {}),
|
|
172
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
async describeTool(name) {
|
|
176
|
+
if (typeof name !== 'string' || !name)
|
|
177
|
+
throw new Error('A tool name is required.');
|
|
178
|
+
let tool = (await this.listTools()).find(item => item.name === name);
|
|
179
|
+
// A new server tool can be called without restarting the local process.
|
|
180
|
+
if (!tool)
|
|
181
|
+
tool = (await this.listTools({ refresh: true })).find(item => item.name === name);
|
|
182
|
+
if (!tool)
|
|
183
|
+
throw new Error('Unknown Steercards tool. Search or refresh the upstream catalog.');
|
|
184
|
+
return tool;
|
|
185
|
+
}
|
|
186
|
+
async callTool(name, args = {}) {
|
|
187
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
188
|
+
throw new Error('Tool arguments must be a JSON object.');
|
|
189
|
+
await this.describeTool(name);
|
|
190
|
+
try {
|
|
191
|
+
// Low-level request deliberately preserves error results without applying an
|
|
192
|
+
// outputSchema intended for successful results. No mutation retry is made.
|
|
193
|
+
return await (await this.connection()).request({
|
|
194
|
+
method: 'tools/call', params: { name, arguments: args },
|
|
195
|
+
}, CallToolResultSchema);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw this.safeError(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async close() {
|
|
202
|
+
this.closed = true;
|
|
203
|
+
const pending = this.connecting;
|
|
204
|
+
// Abort an in-flight initialize as well as established requests before
|
|
205
|
+
// waiting, so closing stdio cannot leave a stalled HTTP handshake running.
|
|
206
|
+
await this.transport?.close();
|
|
207
|
+
if (pending)
|
|
208
|
+
await pending.catch(() => { });
|
|
209
|
+
await this.client?.close();
|
|
210
|
+
this.client = undefined;
|
|
211
|
+
this.transport = undefined;
|
|
212
|
+
this.catalog = undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { Runtime } from './runtime.js';
|
|
3
|
+
export interface ServerOptions {
|
|
4
|
+
mode: 'compact' | 'full';
|
|
5
|
+
}
|
|
6
|
+
export declare function getGuide(topic?: string): Promise<string>;
|
|
7
|
+
/** Low-level Server preserves arbitrary upstream JSON Schemas and tool metadata. */
|
|
8
|
+
export declare function createServer(runtime: Runtime, { mode }: ServerOptions): Server;
|
|
9
|
+
export declare function startServer(runtime: Runtime, options: ServerOptions): Promise<Server>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { packageInfo } from './package.js';
|
|
6
|
+
const guideTopics = ['overview', 'discovery', 'cards', 'profiles', 'claims-runs', 'context', 'files', 'collaboration'];
|
|
7
|
+
const guideUri = 'steercards://guide/';
|
|
8
|
+
const instructions = 'Steercards tools use progressive discovery. In compact mode search tool metadata, describe the selected tool to obtain its current schema, then call it with matching arguments. Read the Steercards guide for workflows. Tool schemas are fetched from the authenticated upstream server. Respect isError and structuredContent. Never automatically retry a mutation after a network failure; read current state first. The upstream rate limit is 120 requests per minute; follow Retry-After.';
|
|
9
|
+
export async function getGuide(topic = 'overview') {
|
|
10
|
+
if (!guideTopics.includes(topic)) {
|
|
11
|
+
throw new Error(`Unknown guide topic. Choose ${guideTopics.join(', ')}.`);
|
|
12
|
+
}
|
|
13
|
+
const path = topic === 'overview' ? '../skills/steercards/SKILL.md' : `../skills/steercards/references/${topic}.md`;
|
|
14
|
+
return readFile(new URL(path, import.meta.url), 'utf8');
|
|
15
|
+
}
|
|
16
|
+
const compactTools = [
|
|
17
|
+
{
|
|
18
|
+
name: 'steercards_search_tools', title: 'Search Steercards tools',
|
|
19
|
+
description: 'Search the live upstream catalog by tool name and description. Returns compact metadata without schemas; describe a match before calling it. An empty query browses the catalog.',
|
|
20
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 } }, required: ['query'], additionalProperties: false },
|
|
21
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'steercards_describe_tool', title: 'Describe a Steercards tool',
|
|
25
|
+
description: 'Get the selected upstream tool’s full current metadata, inputSchema, and outputSchema. The upstream schema is the authority for arguments.',
|
|
26
|
+
inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1 } }, required: ['name'], additionalProperties: false },
|
|
27
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'steercards_call_tool', title: 'Call a Steercards tool',
|
|
31
|
+
description: 'Call a discovered upstream tool by its exact name with a JSON arguments object. Returns the upstream content, structuredContent, and isError unchanged. May write or delete data depending on the selected tool. Never automatically retry an uncertain mutation.',
|
|
32
|
+
inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1 }, arguments: { type: 'object', additionalProperties: true } }, required: ['name', 'arguments'], additionalProperties: false },
|
|
33
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'steercards_get_guide', title: 'Read the Steercards guide',
|
|
37
|
+
description: 'Read agent workflow guidance. Start with overview, then select a topic for the task.',
|
|
38
|
+
inputSchema: { type: 'object', properties: { topic: { type: 'string', enum: [...guideTopics], default: 'overview' } }, additionalProperties: false },
|
|
39
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
function jsonResult(value) {
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value };
|
|
44
|
+
}
|
|
45
|
+
function stringArg(args, name) {
|
|
46
|
+
if (typeof args[name] !== 'string')
|
|
47
|
+
throw new McpError(ErrorCode.InvalidParams, `${name} must be a string.`);
|
|
48
|
+
return args[name];
|
|
49
|
+
}
|
|
50
|
+
function assertKeys(args, keys) {
|
|
51
|
+
if (Object.keys(args).some(key => !keys.includes(key)))
|
|
52
|
+
throw new McpError(ErrorCode.InvalidParams, 'Unexpected argument.');
|
|
53
|
+
}
|
|
54
|
+
/** Low-level Server preserves arbitrary upstream JSON Schemas and tool metadata. */
|
|
55
|
+
export function createServer(runtime, { mode }) {
|
|
56
|
+
if (mode !== 'compact' && mode !== 'full')
|
|
57
|
+
throw new Error('Server mode must be compact or full.');
|
|
58
|
+
const server = new Server({ name: packageInfo.name, version: packageInfo.version }, {
|
|
59
|
+
capabilities: { tools: {}, resources: {} }, instructions,
|
|
60
|
+
});
|
|
61
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
62
|
+
tools: mode === 'compact' ? structuredClone(compactTools) : await runtime.listTools({ refresh: true }),
|
|
63
|
+
}));
|
|
64
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
65
|
+
const args = request.params.arguments ?? {};
|
|
66
|
+
if (mode === 'full')
|
|
67
|
+
return runtime.callTool(request.params.name, args);
|
|
68
|
+
switch (request.params.name) {
|
|
69
|
+
case 'steercards_search_tools': {
|
|
70
|
+
assertKeys(args, ['query', 'limit']);
|
|
71
|
+
const query = stringArg(args, 'query');
|
|
72
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || Number(args.limit) < 1 || Number(args.limit) > 100)) {
|
|
73
|
+
throw new McpError(ErrorCode.InvalidParams, 'limit must be an integer from 1 to 100.');
|
|
74
|
+
}
|
|
75
|
+
return jsonResult({ tools: await runtime.searchTools(query, args.limit) });
|
|
76
|
+
}
|
|
77
|
+
case 'steercards_describe_tool':
|
|
78
|
+
assertKeys(args, ['name']);
|
|
79
|
+
return jsonResult({ tool: await runtime.describeTool(stringArg(args, 'name')) });
|
|
80
|
+
case 'steercards_call_tool': {
|
|
81
|
+
assertKeys(args, ['name', 'arguments']);
|
|
82
|
+
const name = stringArg(args, 'name');
|
|
83
|
+
if (!args.arguments || typeof args.arguments !== 'object' || Array.isArray(args.arguments)) {
|
|
84
|
+
throw new McpError(ErrorCode.InvalidParams, 'arguments must be a JSON object.');
|
|
85
|
+
}
|
|
86
|
+
return runtime.callTool(name, args.arguments);
|
|
87
|
+
}
|
|
88
|
+
case 'steercards_get_guide': {
|
|
89
|
+
assertKeys(args, ['topic']);
|
|
90
|
+
const topic = args.topic === undefined ? 'overview' : stringArg(args, 'topic');
|
|
91
|
+
return jsonResult({ topic, guide: await getGuide(topic) });
|
|
92
|
+
}
|
|
93
|
+
default: throw new McpError(ErrorCode.InvalidParams, 'Unknown local tool.');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Resources cannot collide with any dynamically mirrored upstream tool name.
|
|
97
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
98
|
+
resources: guideTopics.map(topic => ({ uri: `${guideUri}${topic}`, name: `Steercards guide: ${topic}`, mimeType: 'text/markdown' })),
|
|
99
|
+
}));
|
|
100
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
101
|
+
const topic = request.params.uri.startsWith(guideUri) ? request.params.uri.slice(guideUri.length) : '';
|
|
102
|
+
if (!guideTopics.includes(topic))
|
|
103
|
+
throw new McpError(ErrorCode.InvalidParams, 'Unknown guide resource.');
|
|
104
|
+
return { contents: [{ uri: request.params.uri, mimeType: 'text/markdown', text: await getGuide(topic) }] };
|
|
105
|
+
});
|
|
106
|
+
server.onclose = () => { void runtime.close().catch(() => { }); };
|
|
107
|
+
return server;
|
|
108
|
+
}
|
|
109
|
+
export async function startServer(runtime, options) {
|
|
110
|
+
const server = createServer(runtime, options);
|
|
111
|
+
await server.connect(new StdioServerTransport());
|
|
112
|
+
// SDK 1's stdio transport does not close itself on stdin EOF. Explicitly
|
|
113
|
+
// release upstream HTTP resources when the host disconnects.
|
|
114
|
+
const onEnd = () => { void server.close().catch(() => { }); };
|
|
115
|
+
const onClose = server.onclose;
|
|
116
|
+
server.onclose = () => { process.stdin.off('end', onEnd); onClose?.(); };
|
|
117
|
+
process.stdin.once('end', onEnd);
|
|
118
|
+
if (process.stdin.readableEnded)
|
|
119
|
+
onEnd();
|
|
120
|
+
return server;
|
|
121
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SetupOptions {
|
|
2
|
+
client: string;
|
|
3
|
+
cwd?: string;
|
|
4
|
+
local?: boolean;
|
|
5
|
+
mode?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
envFile?: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
home?: string;
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
}
|
|
13
|
+
export interface SetupReport {
|
|
14
|
+
client: string;
|
|
15
|
+
version: string;
|
|
16
|
+
status: 'installed' | 'updated' | 'unchanged' | 'dry-run';
|
|
17
|
+
paths: {
|
|
18
|
+
config: string;
|
|
19
|
+
skill?: string;
|
|
20
|
+
skillZip?: string;
|
|
21
|
+
manifest: string;
|
|
22
|
+
};
|
|
23
|
+
actions: string[];
|
|
24
|
+
nextSteps: string[];
|
|
25
|
+
}
|
|
26
|
+
/** Internal dependency injection for isolated upgrade, race, and failure tests. */
|
|
27
|
+
export interface SetupDependencies {
|
|
28
|
+
source?: string;
|
|
29
|
+
version?: string;
|
|
30
|
+
beforeCommit?: () => Promise<void>;
|
|
31
|
+
beforeWrite?: (path: string, index: number) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** Configure MCP and the matching skill together, preserving unrelated client data. */
|
|
34
|
+
export declare function setup(options: SetupOptions, dependencies?: SetupDependencies): Promise<SetupReport>;
|