@kosuke-ai/cli 1.0.1-next.4 → 1.0.1-next.6
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/README.md +104 -0
- package/dist/commands/mcp.js +85 -0
- package/dist/generated/commands/index.js +43 -39
- package/dist/generated/commands/sessions/artifacts-get.js +20 -0
- package/dist/generated/commands/sessions/artifacts-list.js +19 -0
- package/dist/generated/tools/deploy/cancel.js +20 -0
- package/dist/generated/tools/deploy/messages.js +20 -0
- package/dist/generated/tools/deploy/run.js +52 -0
- package/dist/generated/tools/index.js +138 -0
- package/dist/generated/tools/preview/get.js +24 -0
- package/dist/generated/tools/preview/health.js +24 -0
- package/dist/generated/tools/preview/reset.js +23 -0
- package/dist/generated/tools/preview/share-create.js +24 -0
- package/dist/generated/tools/preview/share-delete.js +24 -0
- package/dist/generated/tools/preview/share-get.js +24 -0
- package/dist/generated/tools/preview/shared-artifact.js +25 -0
- package/dist/generated/tools/preview/shared.js +20 -0
- package/dist/generated/tools/projects/create.js +26 -0
- package/dist/generated/tools/projects/delete.js +20 -0
- package/dist/generated/tools/projects/env-vars-add.js +50 -0
- package/dist/generated/tools/projects/env-vars-remove.js +31 -0
- package/dist/generated/tools/projects/get.js +20 -0
- package/dist/generated/tools/projects/list.js +12 -0
- package/dist/generated/tools/projects/services-add.js +111 -0
- package/dist/generated/tools/projects/services-list.js +20 -0
- package/dist/generated/tools/projects/services-remove.js +25 -0
- package/dist/generated/tools/projects/services-update.js +104 -0
- package/dist/generated/tools/projects/settings-get.js +20 -0
- package/dist/generated/tools/projects/settings-import.js +208 -0
- package/dist/generated/tools/projects/settings-update.js +72 -0
- package/dist/generated/tools/projects/skills.js +32 -0
- package/dist/generated/tools/projects/update.js +24 -0
- package/dist/generated/tools/sessions/activate.js +25 -0
- package/dist/generated/tools/sessions/artifacts-get.js +30 -0
- package/dist/generated/tools/sessions/artifacts-list.js +25 -0
- package/dist/generated/tools/sessions/checklist-generate.js +25 -0
- package/dist/generated/tools/sessions/checklist-get.js +25 -0
- package/dist/generated/tools/sessions/comments-add.js +53 -0
- package/dist/generated/tools/sessions/comments-delete.js +30 -0
- package/dist/generated/tools/sessions/comments-list.js +25 -0
- package/dist/generated/tools/sessions/comments-update.js +62 -0
- package/dist/generated/tools/sessions/create.js +69 -0
- package/dist/generated/tools/sessions/delete.js +25 -0
- package/dist/generated/tools/sessions/diff-files.js +25 -0
- package/dist/generated/tools/sessions/diff-patch.js +64 -0
- package/dist/generated/tools/sessions/edit-prompt.js +69 -0
- package/dist/generated/tools/sessions/files-get.js +30 -0
- package/dist/generated/tools/sessions/files-list.js +25 -0
- package/dist/generated/tools/sessions/files-write.js +37 -0
- package/dist/generated/tools/sessions/get.js +25 -0
- package/dist/generated/tools/sessions/git-commit-message.js +25 -0
- package/dist/generated/tools/sessions/git-commit.js +29 -0
- package/dist/generated/tools/sessions/git-push.js +25 -0
- package/dist/generated/tools/sessions/git-state.js +25 -0
- package/dist/generated/tools/sessions/jobs-cancel.js +30 -0
- package/dist/generated/tools/sessions/jobs-get.js +30 -0
- package/dist/generated/tools/sessions/list.js +56 -0
- package/dist/generated/tools/sessions/messages.js +25 -0
- package/dist/generated/tools/sessions/search.js +17 -0
- package/dist/generated/tools/sessions/send.js +137 -0
- package/dist/generated/tools/sessions/update.js +63 -0
- package/dist/generated/tools/workspaces/access-status.js +28 -0
- package/dist/generated/tools/workspaces/ai-provider-get.js +20 -0
- package/dist/generated/tools/workspaces/ai-provider-remove.js +39 -0
- package/dist/generated/tools/workspaces/ai-provider-set.js +106 -0
- package/dist/generated/tools/workspaces/credits.js +20 -0
- package/dist/generated/tools/workspaces/get.js +20 -0
- package/dist/generated/tools/workspaces/getting-started-get.js +20 -0
- package/dist/generated/tools/workspaces/getting-started-update.js +27 -0
- package/dist/generated/tools/workspaces/list.js +12 -0
- package/dist/generated/tools/workspaces/members.js +24 -0
- package/dist/generated/tools/workspaces/sandbox-limit.js +20 -0
- package/dist/generated/tools/workspaces/stop-sandboxes.js +31 -0
- package/dist/generated/tools/workspaces/usage.js +28 -0
- package/dist/program.js +4 -2
- package/dist/runtime/mcp-attachments.js +110 -0
- package/dist/runtime/mcp-composite-support.js +49 -0
- package/dist/runtime/mcp-composite.js +309 -0
- package/dist/runtime/mcp-guard.js +74 -0
- package/dist/runtime/mcp-invoke.js +123 -0
- package/dist/runtime/mcp-open-pr.js +83 -0
- package/dist/runtime/mcp-redact.js +31 -0
- package/dist/runtime/mcp-server.js +219 -0
- package/dist/runtime/mcp-tool.js +6 -0
- package/openapi.json +134 -0
- package/package.json +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { UsageError } from './invoke.js';
|
|
2
|
+
import { data, failed } from './mcp-composite-support.js';
|
|
3
|
+
/** `anthropic:sonnet__medium` names its provider; anything else does not. */
|
|
4
|
+
function providerOf(model) {
|
|
5
|
+
if (typeof model !== 'string')
|
|
6
|
+
return null;
|
|
7
|
+
const prefix = model.split(':')[0];
|
|
8
|
+
return prefix === 'anthropic' || prefix === 'openai' ? prefix : null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The provider a session's next turn runs on. The session's own model says so
|
|
12
|
+
* once it has one. Before its first message, or when its stored id is no
|
|
13
|
+
* longer recognised, the platform picks the model the way `access-status`
|
|
14
|
+
* reports it for the caller (scope `member`): their saved preference, then
|
|
15
|
+
* their Subscription's provider, then the workspace's credentials. That is
|
|
16
|
+
* the same resolution the send route runs, so its `defaultModel` is the model
|
|
17
|
+
* the turn would get, and it is pinned on the send so it stays that way.
|
|
18
|
+
* `ai-provider` is not consulted: its `defaultModel` is always null there.
|
|
19
|
+
* No model at all is a refusal: a wrong trigger sends the skill as chat text
|
|
20
|
+
* and reports success.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveProvider(session, projectId, context) {
|
|
23
|
+
const own = providerOf(session.model);
|
|
24
|
+
if (own)
|
|
25
|
+
return { provider: own, model: null };
|
|
26
|
+
const project = await context.call('projects_get', { projectId });
|
|
27
|
+
if (!project.ok)
|
|
28
|
+
return { failed: failed(project, 'projects_get') };
|
|
29
|
+
const workspaceId = data(project).orgId;
|
|
30
|
+
if (typeof workspaceId !== 'string' || !workspaceId) {
|
|
31
|
+
throw new UsageError('The project names no workspace, so the provider cannot be resolved.');
|
|
32
|
+
}
|
|
33
|
+
const access = await context.call('workspaces_access_status', { workspaceId, scope: 'member' });
|
|
34
|
+
if (!access.ok)
|
|
35
|
+
return { failed: failed(access, 'workspaces_access_status') };
|
|
36
|
+
const status = data(access);
|
|
37
|
+
const defaultModel = status.defaultModel;
|
|
38
|
+
const provider = providerOf(defaultModel);
|
|
39
|
+
if (provider)
|
|
40
|
+
return { provider, model: defaultModel };
|
|
41
|
+
const why = session.modelUnavailable === true
|
|
42
|
+
? 'its model is no longer available'
|
|
43
|
+
: 'it has not run a turn yet';
|
|
44
|
+
const reason = typeof status.reason === 'string' && status.reason ? ` ${status.reason}` : '';
|
|
45
|
+
throw new UsageError(`This session has no model (${why}) and no provider is usable by you in this workspace, ` +
|
|
46
|
+
`so the provider cannot be told.${reason} Send one message with sessions_send and a ` +
|
|
47
|
+
`model, or connect a provider, then submit again.`);
|
|
48
|
+
}
|
|
49
|
+
/** The skill's own flags; a note that carries one would be read as one. */
|
|
50
|
+
const OPEN_PR_FLAGS = /^(-d|--draft|-r|--reviewers|-e|--evidence)$/;
|
|
51
|
+
export function openPrCommand(trigger, args) {
|
|
52
|
+
const parts = [`${trigger}open-pr`];
|
|
53
|
+
if (args.draft === true)
|
|
54
|
+
parts.push('-d');
|
|
55
|
+
const reviewers = names(args.reviewers, 'reviewers');
|
|
56
|
+
if (reviewers.length)
|
|
57
|
+
parts.push('-r', reviewers.join(','));
|
|
58
|
+
const evidence = names(args.evidence, 'evidence');
|
|
59
|
+
if (evidence.length)
|
|
60
|
+
parts.push('-e', evidence.join(','));
|
|
61
|
+
const notes = typeof args.notes === 'string' ? args.notes.trim() : '';
|
|
62
|
+
if (notes) {
|
|
63
|
+
if (notes.split(/\s+/).some((token) => OPEN_PR_FLAGS.test(token))) {
|
|
64
|
+
throw new UsageError('notes must not carry the skill flags (-d, -r, -e); use draft, reviewers and evidence.');
|
|
65
|
+
}
|
|
66
|
+
parts.push(notes);
|
|
67
|
+
}
|
|
68
|
+
return parts.join(' ');
|
|
69
|
+
}
|
|
70
|
+
/** One name per entry, trimmed; a comma or a space inside one would split it on the way in. */
|
|
71
|
+
function names(value, what) {
|
|
72
|
+
if (!Array.isArray(value))
|
|
73
|
+
return [];
|
|
74
|
+
return value
|
|
75
|
+
.map((item) => String(item).trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((item) => {
|
|
78
|
+
if (/[,\s]/.test(item)) {
|
|
79
|
+
throw new UsageError(`${what} entries are single names or links; "${item}" is not.`);
|
|
80
|
+
}
|
|
81
|
+
return item;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What leaves a tool result. The platform returns a project's secret
|
|
3
|
+
* environment values in plaintext (`isSecret` is a UI hint there) and the
|
|
4
|
+
* project's Sentry DSN on every project row; an MCP result is pasted into a
|
|
5
|
+
* transcript, so both are replaced before anything is returned. Applied to
|
|
6
|
+
* every result, composite or generated, since responses are not modelled in
|
|
7
|
+
* the document and a shape can appear on more than one operation.
|
|
8
|
+
*/
|
|
9
|
+
export const REDACTED = '[redacted]';
|
|
10
|
+
/** `https://<public key>@o123.ingest.sentry.io/456`: the key part is the credential. */
|
|
11
|
+
const SENTRY_DSN = /^https?:\/\/[0-9a-f]{16,}@[^/\s]+\/\d+$/i;
|
|
12
|
+
export function redact(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value.map(redact);
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return SENTRY_DSN.test(value) ? REDACTED : value;
|
|
17
|
+
if (typeof value !== 'object' || value === null)
|
|
18
|
+
return value;
|
|
19
|
+
const record = value;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [key, item] of Object.entries(record)) {
|
|
22
|
+
if (key === 'sentryDsn' && typeof item === 'string')
|
|
23
|
+
out[key] = REDACTED;
|
|
24
|
+
else if (key === 'value' && record.isSecret === true && typeof item === 'string') {
|
|
25
|
+
out[key] = REDACTED;
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
out[key] = redact(item);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
|
|
5
|
+
import { tools as generatedTools } from '../generated/tools/index.js';
|
|
6
|
+
import { createApiClient } from './client.js';
|
|
7
|
+
import { UsageError } from './invoke.js';
|
|
8
|
+
import { defaultAttachmentPolicy } from './mcp-attachments.js';
|
|
9
|
+
import { compositeTools } from './mcp-composite.js';
|
|
10
|
+
import { COMPOSITE_TOOLSET } from './mcp-composite-support.js';
|
|
11
|
+
import { errorResult, invokeTool, toResult, UnknownTool, } from './mcp-invoke.js';
|
|
12
|
+
import { version } from './version.js';
|
|
13
|
+
export const ALL_TOOLSETS = 'all';
|
|
14
|
+
/** The spec's tags, from the generated tools, then the hand-written set. */
|
|
15
|
+
export function availableToolsets() {
|
|
16
|
+
return [...new Set(generatedTools.map((tool) => tool.toolset)), COMPOSITE_TOOLSET];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* GET operations that change state. Each connects to the session's or the
|
|
20
|
+
* project's sandbox, which resumes it when it is paused and starts billing;
|
|
21
|
+
* `preview get` can start a deploy on top when the main sandbox is gone. A
|
|
22
|
+
* method alone cannot tell, so these are named, by their CLI command, and a
|
|
23
|
+
* test holds each name to a real operation.
|
|
24
|
+
*/
|
|
25
|
+
export const MUTATING_READS = new Set([
|
|
26
|
+
'preview get',
|
|
27
|
+
'preview shared',
|
|
28
|
+
'sessions files get',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* What `--read-only` keeps and what `readOnlyHint` says. The hint is what a
|
|
32
|
+
* client trusts to run a call without asking, so it must never be true for
|
|
33
|
+
* a tool that spends money.
|
|
34
|
+
*/
|
|
35
|
+
export function isReadOnly(tool) {
|
|
36
|
+
if (!('operation' in tool))
|
|
37
|
+
return tool.readOnly;
|
|
38
|
+
const { method, command } = tool.operation;
|
|
39
|
+
return method === 'get' && !MUTATING_READS.has(command.join(' '));
|
|
40
|
+
}
|
|
41
|
+
export function selectTools(options) {
|
|
42
|
+
const known = availableToolsets();
|
|
43
|
+
const wanted = options.toolsets?.length ? options.toolsets : [ALL_TOOLSETS];
|
|
44
|
+
for (const name of wanted) {
|
|
45
|
+
if (name !== ALL_TOOLSETS && !known.includes(name)) {
|
|
46
|
+
throw new UsageError(`Unknown toolset "${name}". Known: ${[ALL_TOOLSETS, ...known].join(', ')}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const all = [...generatedTools, ...compositeTools];
|
|
50
|
+
return all.filter((tool) => (wanted.includes(ALL_TOOLSETS) || wanted.includes(tool.toolset)) &&
|
|
51
|
+
(!options.readOnly || isReadOnly(tool)));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What the client is shown per tool; the same for both kinds. A write is
|
|
55
|
+
* flagged destructive, the protocol's own default: nothing here can tell an
|
|
56
|
+
* additive write from one that cancels, resets or archives, and a wrong
|
|
57
|
+
* "additive" is another auto-approval.
|
|
58
|
+
*/
|
|
59
|
+
export function toolListing(tool) {
|
|
60
|
+
const readOnly = isReadOnly(tool);
|
|
61
|
+
return {
|
|
62
|
+
name: tool.name,
|
|
63
|
+
description: tool.description,
|
|
64
|
+
inputSchema: tool.inputSchema,
|
|
65
|
+
annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, openWorldHint: true },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function createMcpServer(options) {
|
|
69
|
+
const log = options.log ?? ((line) => process.stderr.write(line + '\n'));
|
|
70
|
+
const sleep = options.sleep ?? sleepUnlessAborted;
|
|
71
|
+
const now = options.now ?? Date.now;
|
|
72
|
+
const exposed = selectTools(options);
|
|
73
|
+
const byName = new Map(exposed.map((tool) => [tool.name, tool]));
|
|
74
|
+
const generatedByName = new Map(generatedTools.map((tool) => [tool.name, tool]));
|
|
75
|
+
const clients = new Map();
|
|
76
|
+
const clientFor = (anonymous) => {
|
|
77
|
+
let client = clients.get(anonymous);
|
|
78
|
+
if (!client) {
|
|
79
|
+
client = createApiClient({
|
|
80
|
+
apiUrl: options.apiUrl,
|
|
81
|
+
apiKey: anonymous ? null : options.apiKey,
|
|
82
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
83
|
+
});
|
|
84
|
+
clients.set(anonymous, client);
|
|
85
|
+
}
|
|
86
|
+
return client;
|
|
87
|
+
};
|
|
88
|
+
const context = { clientFor, attachments: options.attachments ?? defaultAttachmentPolicy() };
|
|
89
|
+
const validator = new AjvJsonSchemaValidator();
|
|
90
|
+
const validators = new Map();
|
|
91
|
+
const validate = (tool, args) => {
|
|
92
|
+
let check = validators.get(tool.name);
|
|
93
|
+
if (!check) {
|
|
94
|
+
check = validator.getValidator(tool.inputSchema);
|
|
95
|
+
validators.set(tool.name, check);
|
|
96
|
+
}
|
|
97
|
+
return check(args);
|
|
98
|
+
};
|
|
99
|
+
const server = new Server({ name: 'kosuke', version }, {
|
|
100
|
+
capabilities: { tools: {} },
|
|
101
|
+
instructions: 'Tools for the Kosuke platform. `ship`, `watch` and `submit` cover the common arc: ' +
|
|
102
|
+
'start a session, follow its job, open the pull request. Every other tool mirrors one ' +
|
|
103
|
+
'API operation and is named after its CLI command.',
|
|
104
|
+
});
|
|
105
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: exposed.map(toolListing) }));
|
|
106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
107
|
+
const tool = byName.get(request.params.name);
|
|
108
|
+
if (!tool)
|
|
109
|
+
return errorResult(new UnknownTool(request.params.name));
|
|
110
|
+
const args = request.params.arguments ?? {};
|
|
111
|
+
const checked = validate(tool, args);
|
|
112
|
+
if (!checked.valid)
|
|
113
|
+
return errorResult(new UsageError(checked.errorMessage));
|
|
114
|
+
try {
|
|
115
|
+
const invocation = 'operation' in tool
|
|
116
|
+
? await invokeTool(tool, args, context)
|
|
117
|
+
: await tool.run(args, {
|
|
118
|
+
call: async (name, callArgs) => {
|
|
119
|
+
// Composite tools reach the platform only through a generated
|
|
120
|
+
// tool, so their surface is the document's and nothing more,
|
|
121
|
+
// and what they hand on is held to that tool's schema like a
|
|
122
|
+
// call from outside would be.
|
|
123
|
+
const target = generatedByName.get(name);
|
|
124
|
+
if (!target)
|
|
125
|
+
throw new Error(`${tool.name} asked for an unknown tool "${name}".`);
|
|
126
|
+
// Checked here, at the one boundary every sub-call crosses,
|
|
127
|
+
// so no loop path can send a request after the caller has gone.
|
|
128
|
+
if (extra.signal.aborted)
|
|
129
|
+
throw new Cancelled(tool.name, name);
|
|
130
|
+
const inner = validate(target, callArgs);
|
|
131
|
+
if (!inner.valid) {
|
|
132
|
+
throw new UsageError(`${tool.name} → ${name}: ${inner.errorMessage}`);
|
|
133
|
+
}
|
|
134
|
+
return invokeTool(target, callArgs, context);
|
|
135
|
+
},
|
|
136
|
+
progress: async (message, progress, total) => {
|
|
137
|
+
const progressToken = extra._meta?.progressToken;
|
|
138
|
+
if (progressToken === undefined)
|
|
139
|
+
return;
|
|
140
|
+
await extra.sendNotification({
|
|
141
|
+
method: 'notifications/progress',
|
|
142
|
+
params: { progressToken, progress, ...(total ? { total } : {}), message },
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
// A cancelled request must not leave a timer holding the loop.
|
|
146
|
+
sleep: (ms) => abortable(sleep(ms, extra.signal), extra.signal),
|
|
147
|
+
now,
|
|
148
|
+
signal: extra.signal,
|
|
149
|
+
});
|
|
150
|
+
return toResult(invocation);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!(error instanceof UsageError) && !(error instanceof Cancelled)) {
|
|
154
|
+
log(`kosuke mcp: ${tool.name} failed: ${String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
return errorResult(error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return server;
|
|
160
|
+
}
|
|
161
|
+
/** Thrown by a composite sub-call attempted after the caller cancelled; nobody is reading the answer. */
|
|
162
|
+
class Cancelled extends Error {
|
|
163
|
+
constructor(tool, step) {
|
|
164
|
+
super(`${tool} was cancelled before ${step}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The default wait. The timer is cleared when the signal fires, so a
|
|
169
|
+
* cancelled `watch` does not hold a referenced timer for the rest of its poll
|
|
170
|
+
* interval and delay the process exit after the transport closes.
|
|
171
|
+
*/
|
|
172
|
+
export function sleepUnlessAborted(ms, signal) {
|
|
173
|
+
if (signal?.aborted)
|
|
174
|
+
return Promise.resolve();
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const onAbort = () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve();
|
|
179
|
+
};
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
signal?.removeEventListener('abort', onAbort);
|
|
182
|
+
resolve();
|
|
183
|
+
}, ms);
|
|
184
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Settles when the work does, or at once when the signal fires first; for injected sleeps that ignore it. */
|
|
188
|
+
function abortable(work, signal) {
|
|
189
|
+
if (!signal)
|
|
190
|
+
return work;
|
|
191
|
+
if (signal.aborted)
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const onAbort = () => resolve();
|
|
195
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
196
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export async function serveStdio(options) {
|
|
200
|
+
const server = createMcpServer(options);
|
|
201
|
+
const log = options.log ?? ((line) => process.stderr.write(line + '\n'));
|
|
202
|
+
const exposed = selectTools(options);
|
|
203
|
+
const attachments = options.attachments ?? defaultAttachmentPolicy();
|
|
204
|
+
// Both ends of the transport's life are wired before it opens: a transport
|
|
205
|
+
// that errors instead of closing would otherwise hold the process forever.
|
|
206
|
+
const ended = new Promise((resolve) => {
|
|
207
|
+
server.onclose = () => resolve();
|
|
208
|
+
server.onerror = (error) => {
|
|
209
|
+
log(`kosuke mcp: transport error: ${String(error)}`);
|
|
210
|
+
resolve();
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
await server.connect(new StdioServerTransport());
|
|
214
|
+
log(`kosuke mcp: serving ${exposed.length} tools ` +
|
|
215
|
+
`(${(options.toolsets?.length ? options.toolsets : [ALL_TOOLSETS]).join(', ')}` +
|
|
216
|
+
`${options.readOnly ? ', read-only' : ''}) for ${options.apiUrl}; ` +
|
|
217
|
+
`attachments from ${attachments.root}`);
|
|
218
|
+
await ended;
|
|
219
|
+
}
|
package/openapi.json
CHANGED
|
@@ -1733,6 +1733,140 @@
|
|
|
1733
1733
|
}
|
|
1734
1734
|
}
|
|
1735
1735
|
},
|
|
1736
|
+
"/api/projects/{projectId}/chat-sessions/{sessionId}/artifacts": {
|
|
1737
|
+
"get": {
|
|
1738
|
+
"operationId": "sessionsArtifactsList",
|
|
1739
|
+
"summary": "List the session's artifacts, most recently changed first. Rows only: a body or a file url comes from reading one.",
|
|
1740
|
+
"tags": ["sessions"],
|
|
1741
|
+
"x-kosuke-command": ["sessions", "artifacts", "list"],
|
|
1742
|
+
"parameters": [
|
|
1743
|
+
{
|
|
1744
|
+
"name": "projectId",
|
|
1745
|
+
"in": "path",
|
|
1746
|
+
"required": true,
|
|
1747
|
+
"schema": {
|
|
1748
|
+
"type": "string",
|
|
1749
|
+
"format": "uuid",
|
|
1750
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
"name": "sessionId",
|
|
1755
|
+
"in": "path",
|
|
1756
|
+
"required": true,
|
|
1757
|
+
"schema": {
|
|
1758
|
+
"type": "string",
|
|
1759
|
+
"format": "uuid",
|
|
1760
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
],
|
|
1764
|
+
"responses": {
|
|
1765
|
+
"2XX": {
|
|
1766
|
+
"description": "Success envelope",
|
|
1767
|
+
"content": {
|
|
1768
|
+
"application/json": {
|
|
1769
|
+
"schema": {
|
|
1770
|
+
"$ref": "#/components/schemas/ApiSuccess"
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
"4XX": {
|
|
1776
|
+
"description": "Error envelope",
|
|
1777
|
+
"content": {
|
|
1778
|
+
"application/json": {
|
|
1779
|
+
"schema": {
|
|
1780
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
"5XX": {
|
|
1786
|
+
"description": "Error envelope",
|
|
1787
|
+
"content": {
|
|
1788
|
+
"application/json": {
|
|
1789
|
+
"schema": {
|
|
1790
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
"/api/projects/{projectId}/chat-sessions/{sessionId}/artifacts/{artifactId}": {
|
|
1799
|
+
"get": {
|
|
1800
|
+
"operationId": "sessionsArtifactsGet",
|
|
1801
|
+
"summary": "Read one artifact of the session: a markdown or html one carries its body, an image or a video a presigned url, its media type and its size.",
|
|
1802
|
+
"tags": ["sessions"],
|
|
1803
|
+
"x-kosuke-command": ["sessions", "artifacts", "get"],
|
|
1804
|
+
"parameters": [
|
|
1805
|
+
{
|
|
1806
|
+
"name": "projectId",
|
|
1807
|
+
"in": "path",
|
|
1808
|
+
"required": true,
|
|
1809
|
+
"schema": {
|
|
1810
|
+
"type": "string",
|
|
1811
|
+
"format": "uuid",
|
|
1812
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
1813
|
+
}
|
|
1814
|
+
},
|
|
1815
|
+
{
|
|
1816
|
+
"name": "sessionId",
|
|
1817
|
+
"in": "path",
|
|
1818
|
+
"required": true,
|
|
1819
|
+
"schema": {
|
|
1820
|
+
"type": "string",
|
|
1821
|
+
"format": "uuid",
|
|
1822
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
"name": "artifactId",
|
|
1827
|
+
"in": "path",
|
|
1828
|
+
"required": true,
|
|
1829
|
+
"schema": {
|
|
1830
|
+
"type": "string",
|
|
1831
|
+
"format": "uuid",
|
|
1832
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
],
|
|
1836
|
+
"responses": {
|
|
1837
|
+
"2XX": {
|
|
1838
|
+
"description": "Success envelope",
|
|
1839
|
+
"content": {
|
|
1840
|
+
"application/json": {
|
|
1841
|
+
"schema": {
|
|
1842
|
+
"$ref": "#/components/schemas/ApiSuccess"
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
},
|
|
1847
|
+
"4XX": {
|
|
1848
|
+
"description": "Error envelope",
|
|
1849
|
+
"content": {
|
|
1850
|
+
"application/json": {
|
|
1851
|
+
"schema": {
|
|
1852
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
"5XX": {
|
|
1858
|
+
"description": "Error envelope",
|
|
1859
|
+
"content": {
|
|
1860
|
+
"application/json": {
|
|
1861
|
+
"schema": {
|
|
1862
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1736
1870
|
"/api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat": {
|
|
1737
1871
|
"post": {
|
|
1738
1872
|
"operationId": "sessionsSend",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kosuke-ai/cli",
|
|
3
|
-
"version": "1.0.1-next.
|
|
3
|
+
"version": "1.0.1-next.6",
|
|
4
4
|
"description": "Kosuke platform CLI. The client customers install to drive app.kosuke.ai from their own machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
33
|
"commander": "^14.0.0",
|
|
33
34
|
"openapi-fetch": "^0.14.0"
|
|
34
35
|
},
|