offrouter-mcp 0.0.0
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/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +73 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/cancel.d.ts +7 -0
- package/dist/tools/cancel.d.ts.map +1 -0
- package/dist/tools/cancel.js +60 -0
- package/dist/tools/cancel.js.map +1 -0
- package/dist/tools/delegate.d.ts +4 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +195 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/tools/explain.d.ts +8 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +37 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/route.d.ts +4 -0
- package/dist/tools/route.d.ts.map +1 -0
- package/dist/tools/route.js +29 -0
- package/dist/tools/route.js.map +1 -0
- package/dist/tools/status.d.ts +4 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +51 -0
- package/dist/tools/status.js.map +1 -0
- package/package.json +19 -0
- package/package.json.bak +20 -0
- package/src/index.ts +108 -0
- package/src/server.test.ts +538 -0
- package/src/server.ts +126 -0
- package/src/tools/cancel.ts +73 -0
- package/src/tools/delegate.ts +225 -0
- package/src/tools/explain.ts +57 -0
- package/src/tools/route.ts +49 -0
- package/src/tools/status.ts +74 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { OffRouterMcpContext } from "../server.js";
|
|
4
|
+
|
|
5
|
+
const inputSchema = {
|
|
6
|
+
taskId: z.string().min(1),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const outputSchema = {
|
|
10
|
+
cancelled: z.boolean(),
|
|
11
|
+
task: z.record(z.unknown()).optional(),
|
|
12
|
+
error: z
|
|
13
|
+
.object({
|
|
14
|
+
code: z.string(),
|
|
15
|
+
message: z.string(),
|
|
16
|
+
})
|
|
17
|
+
.optional(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cancel a same-process background delegation task started by this MCP server.
|
|
22
|
+
*/
|
|
23
|
+
export function registerCancelTool(
|
|
24
|
+
server: McpServer,
|
|
25
|
+
context: OffRouterMcpContext,
|
|
26
|
+
): void {
|
|
27
|
+
server.registerTool(
|
|
28
|
+
"offrouter_cancel",
|
|
29
|
+
{
|
|
30
|
+
title: "OffRouter cancel",
|
|
31
|
+
description:
|
|
32
|
+
"Cancel an active background delegation task that lives in this MCP server process only.",
|
|
33
|
+
inputSchema,
|
|
34
|
+
outputSchema,
|
|
35
|
+
},
|
|
36
|
+
async (args) => {
|
|
37
|
+
const task = await context.runtime.cancel(args.taskId);
|
|
38
|
+
if (!task) {
|
|
39
|
+
const structuredContent = {
|
|
40
|
+
cancelled: false,
|
|
41
|
+
task: undefined,
|
|
42
|
+
error: {
|
|
43
|
+
code: "unknown_task",
|
|
44
|
+
message: `No active task found for ${args.taskId}.`,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text" as const,
|
|
51
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
structuredContent,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const structuredContent = {
|
|
59
|
+
cancelled: task.status === "cancelled",
|
|
60
|
+
task,
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text" as const,
|
|
66
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
structuredContent,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
DelegationPolicyError,
|
|
4
|
+
RouteRequestSchema,
|
|
5
|
+
type DelegationMode,
|
|
6
|
+
type DelegationEvent,
|
|
7
|
+
type DelegationOutput,
|
|
8
|
+
type DelegationRequest,
|
|
9
|
+
} from "offrouter-core";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { newTaskId, type OffRouterMcpContext } from "../server.js";
|
|
12
|
+
|
|
13
|
+
const ModeSchema = z.enum(["oneshot", "stream", "background"]);
|
|
14
|
+
const OutputSchema = z.enum(["text", "json", "patch"]);
|
|
15
|
+
|
|
16
|
+
const inputSchema = {
|
|
17
|
+
prompt: z.string().min(1),
|
|
18
|
+
route: RouteRequestSchema,
|
|
19
|
+
mode: ModeSchema.default("oneshot"),
|
|
20
|
+
output: OutputSchema.default("text"),
|
|
21
|
+
taskId: z.string().min(1).optional(),
|
|
22
|
+
model: z.string().min(1).optional(),
|
|
23
|
+
providerId: z.string().min(1).optional(),
|
|
24
|
+
maxOutputTokens: z.number().int().positive().optional(),
|
|
25
|
+
temperature: z.number().optional(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const outputSchema = {
|
|
29
|
+
result: z.record(z.unknown()).optional(),
|
|
30
|
+
decision: z.record(z.unknown()).optional(),
|
|
31
|
+
task: z.record(z.unknown()).optional(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function toDelegationRequest(args: {
|
|
35
|
+
prompt: string;
|
|
36
|
+
route: DelegationRequest["route"];
|
|
37
|
+
mode: DelegationMode;
|
|
38
|
+
output: DelegationOutput;
|
|
39
|
+
taskId?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
providerId?: string;
|
|
42
|
+
maxOutputTokens?: number;
|
|
43
|
+
temperature?: number;
|
|
44
|
+
}): DelegationRequest {
|
|
45
|
+
return {
|
|
46
|
+
taskId: args.taskId ?? newTaskId(),
|
|
47
|
+
mode: args.mode,
|
|
48
|
+
output: args.output,
|
|
49
|
+
prompt: args.prompt,
|
|
50
|
+
route: args.route,
|
|
51
|
+
model: args.model,
|
|
52
|
+
providerId: args.providerId,
|
|
53
|
+
maxOutputTokens: args.maxOutputTokens,
|
|
54
|
+
temperature: args.temperature,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function registerDelegateTool(
|
|
59
|
+
server: McpServer,
|
|
60
|
+
context: OffRouterMcpContext,
|
|
61
|
+
): void {
|
|
62
|
+
server.registerTool(
|
|
63
|
+
"offrouter_delegate",
|
|
64
|
+
{
|
|
65
|
+
title: "OffRouter delegate",
|
|
66
|
+
description:
|
|
67
|
+
"Route then invoke an allowed provider. Full prompt is only sent after policy allows. Background tasks run only in this MCP process.",
|
|
68
|
+
inputSchema,
|
|
69
|
+
outputSchema,
|
|
70
|
+
},
|
|
71
|
+
async (args) => {
|
|
72
|
+
const mode = (args.mode ?? "oneshot") as DelegationMode;
|
|
73
|
+
const output = (args.output ?? "text") as DelegationOutput;
|
|
74
|
+
const request = toDelegationRequest({
|
|
75
|
+
prompt: args.prompt,
|
|
76
|
+
route: args.route,
|
|
77
|
+
mode,
|
|
78
|
+
output,
|
|
79
|
+
taskId: args.taskId,
|
|
80
|
+
model: args.model,
|
|
81
|
+
providerId: args.providerId,
|
|
82
|
+
maxOutputTokens: args.maxOutputTokens,
|
|
83
|
+
temperature: args.temperature,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (mode === "background") {
|
|
87
|
+
try {
|
|
88
|
+
const task = await context.runtime.startBackground(request);
|
|
89
|
+
const structuredContent = { task };
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text" as const,
|
|
94
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
structuredContent,
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof DelegationPolicyError) {
|
|
101
|
+
const structuredContent = {
|
|
102
|
+
task: {
|
|
103
|
+
taskId: request.taskId,
|
|
104
|
+
status: "failed",
|
|
105
|
+
},
|
|
106
|
+
decision: err.decision,
|
|
107
|
+
result: {
|
|
108
|
+
status: "failed",
|
|
109
|
+
error: err.message,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text" as const,
|
|
116
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
structuredContent,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (mode === "stream") {
|
|
127
|
+
try {
|
|
128
|
+
const events: DelegationEvent[] = [];
|
|
129
|
+
for await (const event of context.runtime.stream(request)) {
|
|
130
|
+
events.push(event);
|
|
131
|
+
}
|
|
132
|
+
const task = context.runtime.getTask(request.taskId);
|
|
133
|
+
const structuredContent = {
|
|
134
|
+
result: {
|
|
135
|
+
status: task?.status ?? "failed",
|
|
136
|
+
text: task?.text,
|
|
137
|
+
error: task?.error,
|
|
138
|
+
taskId: request.taskId,
|
|
139
|
+
events,
|
|
140
|
+
},
|
|
141
|
+
decision: task?.decision,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text" as const,
|
|
147
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
structuredContent,
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err instanceof DelegationPolicyError) {
|
|
154
|
+
const structuredContent = {
|
|
155
|
+
result: {
|
|
156
|
+
status: "failed",
|
|
157
|
+
error: err.message,
|
|
158
|
+
taskId: request.taskId,
|
|
159
|
+
events: context.runtime.getTask(request.taskId)?.events ?? [],
|
|
160
|
+
},
|
|
161
|
+
decision: err.decision,
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text" as const,
|
|
167
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
structuredContent,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// oneshot
|
|
178
|
+
try {
|
|
179
|
+
const result = await context.runtime.delegate({
|
|
180
|
+
...request,
|
|
181
|
+
mode: "oneshot",
|
|
182
|
+
});
|
|
183
|
+
const structuredContent = {
|
|
184
|
+
result: {
|
|
185
|
+
status: result.status,
|
|
186
|
+
text: result.text,
|
|
187
|
+
error: result.error,
|
|
188
|
+
taskId: result.taskId,
|
|
189
|
+
},
|
|
190
|
+
decision: result.decision,
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: "text" as const,
|
|
196
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
structuredContent,
|
|
200
|
+
};
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err instanceof DelegationPolicyError) {
|
|
203
|
+
const structuredContent = {
|
|
204
|
+
result: {
|
|
205
|
+
status: "failed",
|
|
206
|
+
error: err.message,
|
|
207
|
+
taskId: request.taskId,
|
|
208
|
+
},
|
|
209
|
+
decision: err.decision,
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text" as const,
|
|
215
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
structuredContent,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
RouteRequestSchema,
|
|
4
|
+
routeRequest,
|
|
5
|
+
type RouteDecision,
|
|
6
|
+
} from "offrouter-core";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { OffRouterMcpContext } from "../server.js";
|
|
9
|
+
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
route: RouteRequestSchema,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const outputSchema = {
|
|
15
|
+
decision: z.record(z.unknown()),
|
|
16
|
+
explanation: z.string(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* offrouter_explain_decision — same scoring path as route, named for harness
|
|
21
|
+
* agents that want an explicit explain call.
|
|
22
|
+
*/
|
|
23
|
+
export function registerExplainDecisionTool(
|
|
24
|
+
server: McpServer,
|
|
25
|
+
context: OffRouterMcpContext,
|
|
26
|
+
): void {
|
|
27
|
+
server.registerTool(
|
|
28
|
+
"offrouter_explain_decision",
|
|
29
|
+
{
|
|
30
|
+
title: "OffRouter explain decision",
|
|
31
|
+
description:
|
|
32
|
+
"Explain why OffRouter would choose (or block) a route for the given request.",
|
|
33
|
+
inputSchema,
|
|
34
|
+
outputSchema,
|
|
35
|
+
},
|
|
36
|
+
async (args) => {
|
|
37
|
+
const decision: RouteDecision = routeRequest(
|
|
38
|
+
args.route,
|
|
39
|
+
context.candidates,
|
|
40
|
+
context.policy,
|
|
41
|
+
);
|
|
42
|
+
const structuredContent = {
|
|
43
|
+
decision,
|
|
44
|
+
explanation: decision.explanation,
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text" as const,
|
|
50
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
structuredContent,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
RouteRequestSchema,
|
|
4
|
+
routeRequest,
|
|
5
|
+
type RouteDecision,
|
|
6
|
+
} from "offrouter-core";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { OffRouterMcpContext } from "../server.js";
|
|
9
|
+
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
route: RouteRequestSchema,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const outputSchema = {
|
|
15
|
+
decision: z.record(z.unknown()),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function registerRouteTool(
|
|
19
|
+
server: McpServer,
|
|
20
|
+
context: OffRouterMcpContext,
|
|
21
|
+
): void {
|
|
22
|
+
server.registerTool(
|
|
23
|
+
"offrouter_route",
|
|
24
|
+
{
|
|
25
|
+
title: "OffRouter route",
|
|
26
|
+
description:
|
|
27
|
+
"Select a model route for a request using OffRouter policy and ranking. Uses prompt digest/preview only.",
|
|
28
|
+
inputSchema,
|
|
29
|
+
outputSchema,
|
|
30
|
+
},
|
|
31
|
+
async (args) => {
|
|
32
|
+
const decision: RouteDecision = routeRequest(
|
|
33
|
+
args.route,
|
|
34
|
+
context.candidates,
|
|
35
|
+
context.policy,
|
|
36
|
+
);
|
|
37
|
+
const structuredContent = { decision };
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text" as const,
|
|
42
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
structuredContent,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { OffRouterMcpContext } from "../server.js";
|
|
4
|
+
|
|
5
|
+
const inputSchema = {};
|
|
6
|
+
|
|
7
|
+
const outputSchema = {
|
|
8
|
+
status: z.object({
|
|
9
|
+
configuredProfiles: z.array(z.string()),
|
|
10
|
+
configuredProviders: z.array(z.string()),
|
|
11
|
+
registeredAdapters: z.array(z.string()),
|
|
12
|
+
invokableProviders: z.array(z.string()),
|
|
13
|
+
configuredButNotInvokableProviders: z.array(z.string()),
|
|
14
|
+
candidateCount: z.number().int().nonnegative(),
|
|
15
|
+
activeTasks: z.number().int().nonnegative(),
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function registerStatusTool(
|
|
20
|
+
server: McpServer,
|
|
21
|
+
context: OffRouterMcpContext,
|
|
22
|
+
): void {
|
|
23
|
+
server.registerTool(
|
|
24
|
+
"offrouter_status",
|
|
25
|
+
{
|
|
26
|
+
title: "OffRouter status",
|
|
27
|
+
description:
|
|
28
|
+
"Report configured profiles/providers and active in-process delegation task count for this MCP server.",
|
|
29
|
+
inputSchema,
|
|
30
|
+
outputSchema,
|
|
31
|
+
},
|
|
32
|
+
async () => {
|
|
33
|
+
const activeTasks = context.runtime
|
|
34
|
+
.listTasks()
|
|
35
|
+
.filter((t) => t.status === "running" || t.status === "queued").length;
|
|
36
|
+
const registeredAdapters = Object.keys(context.adapters).sort((a, b) =>
|
|
37
|
+
a.localeCompare(b),
|
|
38
|
+
);
|
|
39
|
+
const candidateProviders = new Set(
|
|
40
|
+
context.candidates.map((candidate) => candidate.providerId),
|
|
41
|
+
);
|
|
42
|
+
const invokableProviders = registeredAdapters.filter((providerId) =>
|
|
43
|
+
candidateProviders.has(providerId),
|
|
44
|
+
);
|
|
45
|
+
const invokable = new Set(invokableProviders);
|
|
46
|
+
const configuredButNotInvokableProviders =
|
|
47
|
+
context.configuredProviders.filter(
|
|
48
|
+
(providerId) => !invokable.has(providerId),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const structuredContent = {
|
|
52
|
+
status: {
|
|
53
|
+
configuredProfiles: [...context.configuredProfiles],
|
|
54
|
+
configuredProviders: [...context.configuredProviders],
|
|
55
|
+
registeredAdapters,
|
|
56
|
+
invokableProviders,
|
|
57
|
+
configuredButNotInvokableProviders,
|
|
58
|
+
candidateCount: context.candidates.length,
|
|
59
|
+
activeTasks,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
structuredContent,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}
|