@tymio/mcp-server 1.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/README.md +84 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +283 -0
- package/dist/mcpFeedbackFooter.d.ts +7 -0
- package/dist/mcpFeedbackFooter.js +33 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Tymio MCP server (stdio)
|
|
2
|
+
|
|
3
|
+
Local **stdio** MCP server for the Tymio hub: exposes REST APIs as [MCP](https://modelcontextprotocol.io/) tools using a **Bearer API key**. Use for scripts, CI, or when you do not want remote OAuth.
|
|
4
|
+
|
|
5
|
+
**Remote MCP** (recommended for daily Cursor use) runs inside the main Express app at `POST /mcp` with OAuth 2.1 and Google. See **[docs/HUB.md](../docs/HUB.md)** §6 for architecture, Google callback URL, and Cursor config (local + remote).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
1. Server env: `API_KEY` set; optional `API_KEY_USER_ID` (otherwise first `SUPER_ADMIN` is used).
|
|
12
|
+
2. Tymio API running (e.g. `npm run dev` from repo root).
|
|
13
|
+
|
|
14
|
+
## Environment
|
|
15
|
+
|
|
16
|
+
| Variable | Required | Description |
|
|
17
|
+
|----------|----------|-------------|
|
|
18
|
+
| `DRD_API_BASE_URL` | No (default `http://localhost:8080`) | Hub API base URL |
|
|
19
|
+
| `DRD_API_KEY` | Yes for authenticated tools | Same value as server `API_KEY` |
|
|
20
|
+
|
|
21
|
+
## Install globally (npm)
|
|
22
|
+
|
|
23
|
+
After the package is [published](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages) to the `@tymio` scope (or from a local checkout):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @tymio/mcp-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This installs the **`tymio-mcp`** command on your `PATH`. Until it is on the registry, install from the repo:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g /absolute/path/to/proproman/mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Build and run
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run mcp:build
|
|
39
|
+
npm run mcp:start
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or from `mcp/`: `npm run build` && `npm run start`. The process uses **stdio**; it is spawned by the MCP client, not run interactively.
|
|
43
|
+
|
|
44
|
+
## Cursor (stdio)
|
|
45
|
+
|
|
46
|
+
With a global install, point the client at the `tymio-mcp` binary:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"tymio-local": {
|
|
52
|
+
"command": "tymio-mcp",
|
|
53
|
+
"args": [],
|
|
54
|
+
"env": {
|
|
55
|
+
"DRD_API_BASE_URL": "http://localhost:8080",
|
|
56
|
+
"DRD_API_KEY": "same-as-server-API_KEY"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Without a global install, use `node` and the built file:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"tymio-local": {
|
|
69
|
+
"command": "node",
|
|
70
|
+
"args": ["/ABSOLUTE/PATH/TO/repo/mcp/dist/index.js"],
|
|
71
|
+
"env": {
|
|
72
|
+
"DRD_API_BASE_URL": "http://localhost:8080",
|
|
73
|
+
"DRD_API_KEY": "same-as-server-API_KEY"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Tools
|
|
81
|
+
|
|
82
|
+
**Ontology / playbook (Tymio):** `tymio_get_coding_agent_guide` (full [CODING_AGENT_TYMIO.md](../docs/CODING_AGENT_TYMIO.md) from server), `tymio_get_agent_brief`, `tymio_list_capabilities`, `tymio_get_capability` — see [docs/HUB.md](../docs/HUB.md) §6.1.
|
|
83
|
+
|
|
84
|
+
**Backlog / data (historical `drd_*` prefix):** health, meta, initiatives, features, requirements, domains, products, accounts, partners, demands, campaigns, timeline, assignments, stakeholders, etc. Full list: `server/src/mcp/tools.ts` and `mcp/src/index.ts` (stdio subset).
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Tymio hub API client for the stdio MCP server. Uses DRD_API_BASE_URL and DRD_API_KEY from env.
|
|
3
|
+
*/
|
|
4
|
+
/** JSON-friendly body; plain objects are stringified. */
|
|
5
|
+
export type DrdFetchInit = Omit<RequestInit, "body"> & {
|
|
6
|
+
body?: string | Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export declare function drdFetch<T>(path: string, init?: DrdFetchInit): Promise<T>;
|
|
9
|
+
/** Plain text body (e.g. Markdown agent brief). */
|
|
10
|
+
export declare function drdFetchText(path: string, init?: RequestInit): Promise<string>;
|
|
11
|
+
export declare function getBaseUrl(): string;
|
|
12
|
+
export declare function hasApiKey(): boolean;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Tymio hub API client for the stdio MCP server. Uses DRD_API_BASE_URL and DRD_API_KEY from env.
|
|
3
|
+
*/
|
|
4
|
+
const baseUrl = process.env.DRD_API_BASE_URL ?? "http://localhost:8080";
|
|
5
|
+
const apiKey = process.env.DRD_API_KEY ?? process.env.API_KEY ?? "";
|
|
6
|
+
function headers() {
|
|
7
|
+
const h = { "Content-Type": "application/json" };
|
|
8
|
+
if (apiKey)
|
|
9
|
+
h["Authorization"] = `Bearer ${apiKey}`;
|
|
10
|
+
return h;
|
|
11
|
+
}
|
|
12
|
+
export async function drdFetch(path, init) {
|
|
13
|
+
const { body, ...rest } = init ?? {};
|
|
14
|
+
const bodyInit = body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body);
|
|
15
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
16
|
+
...rest,
|
|
17
|
+
body: bodyInit,
|
|
18
|
+
headers: { ...headers(), ...(rest.headers ?? {}) }
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const body = await res.text();
|
|
22
|
+
throw new Error(`Tymio API ${res.status}: ${body || res.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
if (res.status === 204)
|
|
25
|
+
return undefined;
|
|
26
|
+
return (await res.json());
|
|
27
|
+
}
|
|
28
|
+
/** Plain text body (e.g. Markdown agent brief). */
|
|
29
|
+
export async function drdFetchText(path, init) {
|
|
30
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
31
|
+
...init,
|
|
32
|
+
headers: { ...headers(), ...(init?.headers ?? {}) }
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const errBody = await res.text();
|
|
36
|
+
throw new Error(`Tymio API ${res.status}: ${errBody || res.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
return res.text();
|
|
39
|
+
}
|
|
40
|
+
export function getBaseUrl() {
|
|
41
|
+
return baseUrl;
|
|
42
|
+
}
|
|
43
|
+
export function hasApiKey() {
|
|
44
|
+
return Boolean(apiKey);
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tymio MCP server (stdio) — exposes hub REST APIs as MCP tools for agents.
|
|
4
|
+
* Set DRD_API_BASE_URL and DRD_API_KEY in the environment.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { drdFetch, drdFetchText, getBaseUrl, hasApiKey } from "./api.js";
|
|
10
|
+
import { toolTextWithFeedback } from "./mcpFeedbackFooter.js";
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: "tymio-hub",
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
});
|
|
15
|
+
async function textContent(text) {
|
|
16
|
+
return toolTextWithFeedback(getBaseUrl(), text);
|
|
17
|
+
}
|
|
18
|
+
// --- Health & meta (no auth required for health)
|
|
19
|
+
server.registerTool("drd_health", {
|
|
20
|
+
title: "Tymio API health check",
|
|
21
|
+
description: "Check if the Tymio hub API is reachable.",
|
|
22
|
+
inputSchema: z.object({})
|
|
23
|
+
}, async () => {
|
|
24
|
+
const data = await drdFetch("/api/health");
|
|
25
|
+
return textContent(JSON.stringify(data));
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("drd_meta", {
|
|
28
|
+
title: "Get Tymio meta",
|
|
29
|
+
description: "Get meta data: domains, products, accounts, partners, personas, revenue streams, users.",
|
|
30
|
+
inputSchema: z.object({})
|
|
31
|
+
}, async () => {
|
|
32
|
+
const data = await drdFetch("/api/meta");
|
|
33
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
34
|
+
});
|
|
35
|
+
// --- Initiatives
|
|
36
|
+
const listInitiativesSchema = z.object({
|
|
37
|
+
domainId: z.string().optional(),
|
|
38
|
+
ownerId: z.string().optional(),
|
|
39
|
+
horizon: z.enum(["NOW", "NEXT", "LATER"]).optional(),
|
|
40
|
+
priority: z.enum(["P0", "P1", "P2", "P3"]).optional(),
|
|
41
|
+
isGap: z.boolean().optional()
|
|
42
|
+
});
|
|
43
|
+
server.registerTool("drd_list_initiatives", {
|
|
44
|
+
title: "List initiatives",
|
|
45
|
+
description: "List initiatives with optional filters: domainId, ownerId, horizon, priority, isGap.",
|
|
46
|
+
inputSchema: listInitiativesSchema
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
const params = new URLSearchParams();
|
|
49
|
+
if (args.domainId)
|
|
50
|
+
params.set("domainId", args.domainId);
|
|
51
|
+
if (args.ownerId)
|
|
52
|
+
params.set("ownerId", args.ownerId);
|
|
53
|
+
if (args.horizon)
|
|
54
|
+
params.set("horizon", args.horizon);
|
|
55
|
+
if (args.priority)
|
|
56
|
+
params.set("priority", args.priority);
|
|
57
|
+
if (args.isGap !== undefined)
|
|
58
|
+
params.set("isGap", String(args.isGap));
|
|
59
|
+
const data = await drdFetch(`/api/initiatives?${params.toString()}`);
|
|
60
|
+
return textContent(JSON.stringify(data.initiatives, null, 2));
|
|
61
|
+
});
|
|
62
|
+
server.registerTool("drd_get_initiative", {
|
|
63
|
+
title: "Get initiative by ID",
|
|
64
|
+
description: "Get a single initiative by its ID.",
|
|
65
|
+
inputSchema: z.object({ id: z.string().describe("Initiative ID") })
|
|
66
|
+
}, async ({ id }) => {
|
|
67
|
+
const data = await drdFetch(`/api/initiatives/${id}`);
|
|
68
|
+
return textContent(JSON.stringify(data.initiative, null, 2));
|
|
69
|
+
});
|
|
70
|
+
server.registerTool("drd_create_initiative", {
|
|
71
|
+
title: "Create initiative",
|
|
72
|
+
description: "Create a new initiative. Requires admin/editor role.",
|
|
73
|
+
inputSchema: z.object({
|
|
74
|
+
title: z.string(),
|
|
75
|
+
domainId: z.string(),
|
|
76
|
+
description: z.string().optional(),
|
|
77
|
+
ownerId: z.string().optional(),
|
|
78
|
+
priority: z.enum(["P0", "P1", "P2", "P3"]).optional(),
|
|
79
|
+
horizon: z.enum(["NOW", "NEXT", "LATER"]).optional(),
|
|
80
|
+
status: z.enum(["IDEA", "PLANNED", "IN_PROGRESS", "DONE", "BLOCKED"]).optional(),
|
|
81
|
+
commercialType: z.string().optional(),
|
|
82
|
+
isGap: z.boolean().optional()
|
|
83
|
+
})
|
|
84
|
+
}, async (body) => {
|
|
85
|
+
const data = await drdFetch("/api/initiatives", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: JSON.stringify(body)
|
|
88
|
+
});
|
|
89
|
+
return textContent(JSON.stringify(data.initiative, null, 2));
|
|
90
|
+
});
|
|
91
|
+
server.registerTool("drd_update_initiative", {
|
|
92
|
+
title: "Update initiative",
|
|
93
|
+
description: "Update an existing initiative by ID.",
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
id: z.string(),
|
|
96
|
+
title: z.string().optional(),
|
|
97
|
+
domainId: z.string().optional(),
|
|
98
|
+
description: z.string().optional(),
|
|
99
|
+
ownerId: z.string().optional(),
|
|
100
|
+
priority: z.enum(["P0", "P1", "P2", "P3"]).optional(),
|
|
101
|
+
horizon: z.enum(["NOW", "NEXT", "LATER"]).optional(),
|
|
102
|
+
status: z.enum(["IDEA", "PLANNED", "IN_PROGRESS", "DONE", "BLOCKED"]).optional(),
|
|
103
|
+
commercialType: z.string().optional(),
|
|
104
|
+
isGap: z.boolean().optional()
|
|
105
|
+
})
|
|
106
|
+
}, async ({ id, ...body }) => {
|
|
107
|
+
const data = await drdFetch(`/api/initiatives/${id}`, {
|
|
108
|
+
method: "PUT",
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
return textContent(JSON.stringify(data.initiative, null, 2));
|
|
112
|
+
});
|
|
113
|
+
server.registerTool("drd_delete_initiative", {
|
|
114
|
+
title: "Delete initiative",
|
|
115
|
+
description: "Delete an initiative by ID.",
|
|
116
|
+
inputSchema: z.object({ id: z.string() })
|
|
117
|
+
}, async ({ id }) => {
|
|
118
|
+
await drdFetch(`/api/initiatives/${id}`, { method: "DELETE" });
|
|
119
|
+
return textContent(JSON.stringify({ ok: true }));
|
|
120
|
+
});
|
|
121
|
+
// --- Domains, products, personas
|
|
122
|
+
server.registerTool("drd_list_domains", {
|
|
123
|
+
title: "List domains",
|
|
124
|
+
description: "List all domains.",
|
|
125
|
+
inputSchema: z.object({})
|
|
126
|
+
}, async () => {
|
|
127
|
+
const data = await drdFetch("/api/domains");
|
|
128
|
+
return textContent(JSON.stringify(data.domains, null, 2));
|
|
129
|
+
});
|
|
130
|
+
server.registerTool("drd_create_domain", {
|
|
131
|
+
title: "Create domain",
|
|
132
|
+
description: "Create a new domain (pillar). Requires workspace OWNER or ADMIN.",
|
|
133
|
+
inputSchema: z.object({
|
|
134
|
+
name: z.string().min(1),
|
|
135
|
+
color: z.string().min(1),
|
|
136
|
+
sortOrder: z.number().int().optional()
|
|
137
|
+
})
|
|
138
|
+
}, async (body) => {
|
|
139
|
+
const data = await drdFetch("/api/domains", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
name: body.name,
|
|
143
|
+
color: body.color,
|
|
144
|
+
sortOrder: body.sortOrder ?? 0
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
return textContent(JSON.stringify(data.domain, null, 2));
|
|
148
|
+
});
|
|
149
|
+
server.registerTool("drd_list_products", {
|
|
150
|
+
title: "List products",
|
|
151
|
+
description: "List all products (with hierarchy).",
|
|
152
|
+
inputSchema: z.object({})
|
|
153
|
+
}, async () => {
|
|
154
|
+
const data = await drdFetch("/api/products");
|
|
155
|
+
return textContent(JSON.stringify(data.products, null, 2));
|
|
156
|
+
});
|
|
157
|
+
server.registerTool("drd_list_personas", {
|
|
158
|
+
title: "List personas",
|
|
159
|
+
description: "List all personas.",
|
|
160
|
+
inputSchema: z.object({})
|
|
161
|
+
}, async () => {
|
|
162
|
+
const data = await drdFetch("/api/personas");
|
|
163
|
+
return textContent(JSON.stringify(data.personas, null, 2));
|
|
164
|
+
});
|
|
165
|
+
server.registerTool("drd_list_accounts", {
|
|
166
|
+
title: "List accounts",
|
|
167
|
+
description: "List all accounts.",
|
|
168
|
+
inputSchema: z.object({})
|
|
169
|
+
}, async () => {
|
|
170
|
+
const data = await drdFetch("/api/accounts");
|
|
171
|
+
return textContent(JSON.stringify(data.accounts, null, 2));
|
|
172
|
+
});
|
|
173
|
+
server.registerTool("drd_list_partners", {
|
|
174
|
+
title: "List partners",
|
|
175
|
+
description: "List all partners.",
|
|
176
|
+
inputSchema: z.object({})
|
|
177
|
+
}, async () => {
|
|
178
|
+
const data = await drdFetch("/api/partners");
|
|
179
|
+
return textContent(JSON.stringify(data.partners, null, 2));
|
|
180
|
+
});
|
|
181
|
+
// --- KPIs, milestones, stakeholders
|
|
182
|
+
server.registerTool("drd_list_kpis", {
|
|
183
|
+
title: "List KPIs",
|
|
184
|
+
description: "List all initiative KPIs with their initiative context (title, domain, owner).",
|
|
185
|
+
inputSchema: z.object({})
|
|
186
|
+
}, async () => {
|
|
187
|
+
const data = await drdFetch("/api/kpis");
|
|
188
|
+
return textContent(JSON.stringify(data.kpis, null, 2));
|
|
189
|
+
});
|
|
190
|
+
server.registerTool("drd_list_milestones", {
|
|
191
|
+
title: "List milestones",
|
|
192
|
+
description: "List all initiative milestones with their initiative context.",
|
|
193
|
+
inputSchema: z.object({})
|
|
194
|
+
}, async () => {
|
|
195
|
+
const data = await drdFetch("/api/milestones");
|
|
196
|
+
return textContent(JSON.stringify(data.milestones, null, 2));
|
|
197
|
+
});
|
|
198
|
+
server.registerTool("drd_list_demands", {
|
|
199
|
+
title: "List demands",
|
|
200
|
+
description: "List all demands (from accounts, partners, internal, compliance).",
|
|
201
|
+
inputSchema: z.object({})
|
|
202
|
+
}, async () => {
|
|
203
|
+
const data = await drdFetch("/api/demands");
|
|
204
|
+
return textContent(JSON.stringify(data.demands, null, 2));
|
|
205
|
+
});
|
|
206
|
+
server.registerTool("drd_list_revenue_streams", {
|
|
207
|
+
title: "List revenue streams",
|
|
208
|
+
description: "List all revenue streams.",
|
|
209
|
+
inputSchema: z.object({})
|
|
210
|
+
}, async () => {
|
|
211
|
+
const data = await drdFetch("/api/revenue-streams");
|
|
212
|
+
return textContent(JSON.stringify(data.revenueStreams, null, 2));
|
|
213
|
+
});
|
|
214
|
+
server.registerTool("tymio_get_coding_agent_guide", {
|
|
215
|
+
title: "Get Tymio coding agent playbook (Markdown)",
|
|
216
|
+
description: "Full docs/CODING_AGENT_TYMIO.md: MCP usage, as-is to Tymio, feature lifecycle. Call at session start when automating this hub.",
|
|
217
|
+
inputSchema: z.object({})
|
|
218
|
+
}, async () => {
|
|
219
|
+
const md = await drdFetchText("/api/agent/coding-guide");
|
|
220
|
+
return textContent(md);
|
|
221
|
+
});
|
|
222
|
+
server.registerTool("tymio_get_agent_brief", {
|
|
223
|
+
title: "Get compiled agent capability brief",
|
|
224
|
+
description: "Returns the hub capability ontology as Markdown or JSON. mode=compact|full, format=md|json.",
|
|
225
|
+
inputSchema: z.object({
|
|
226
|
+
mode: z.enum(["compact", "full"]).default("compact"),
|
|
227
|
+
format: z.enum(["md", "json"]).default("md")
|
|
228
|
+
})
|
|
229
|
+
}, async (args) => {
|
|
230
|
+
const params = new URLSearchParams({ mode: args.mode, format: args.format });
|
|
231
|
+
const q = params.toString();
|
|
232
|
+
if (args.format === "md") {
|
|
233
|
+
const text = await drdFetchText(`/api/ontology/brief?${q}`);
|
|
234
|
+
return textContent(text);
|
|
235
|
+
}
|
|
236
|
+
const raw = await drdFetchText(`/api/ontology/brief?${q}`);
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(raw);
|
|
239
|
+
return textContent(JSON.stringify(parsed, null, 2));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return textContent(raw);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
server.registerTool("tymio_list_capabilities", {
|
|
246
|
+
title: "List hub capabilities (ontology)",
|
|
247
|
+
description: "Optional status: ACTIVE, DRAFT, DEPRECATED.",
|
|
248
|
+
inputSchema: z.object({ status: z.enum(["ACTIVE", "DRAFT", "DEPRECATED"]).optional() })
|
|
249
|
+
}, async (args) => {
|
|
250
|
+
const params = new URLSearchParams();
|
|
251
|
+
if (args.status)
|
|
252
|
+
params.set("status", args.status);
|
|
253
|
+
const q = params.toString();
|
|
254
|
+
const data = await drdFetch(`/api/ontology/capabilities${q ? `?${q}` : ""}`);
|
|
255
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
256
|
+
});
|
|
257
|
+
server.registerTool("tymio_get_capability", {
|
|
258
|
+
title: "Get one capability by id or slug",
|
|
259
|
+
description: "Provide id or slug.",
|
|
260
|
+
inputSchema: z.object({ id: z.string().optional(), slug: z.string().optional() })
|
|
261
|
+
}, async (args) => {
|
|
262
|
+
if (args.id) {
|
|
263
|
+
const data = await drdFetch(`/api/ontology/capabilities/${args.id}`);
|
|
264
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
265
|
+
}
|
|
266
|
+
if (args.slug) {
|
|
267
|
+
const data = await drdFetch(`/api/ontology/capabilities/by-slug/${encodeURIComponent(args.slug)}`);
|
|
268
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
269
|
+
}
|
|
270
|
+
throw new Error("Provide id or slug");
|
|
271
|
+
});
|
|
272
|
+
// --- Run
|
|
273
|
+
async function main() {
|
|
274
|
+
if (!hasApiKey()) {
|
|
275
|
+
process.stderr.write("Warning: DRD_API_KEY is not set. Authenticated API calls will fail. Set DRD_API_KEY and API_KEY on the server.\n");
|
|
276
|
+
}
|
|
277
|
+
const transport = new StdioServerTransport();
|
|
278
|
+
await server.connect(transport);
|
|
279
|
+
}
|
|
280
|
+
main().catch((err) => {
|
|
281
|
+
console.error(err);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches /api/mcp/agent-context (no auth) once and appends feedback instructions to every tool result.
|
|
3
|
+
*/
|
|
4
|
+
let cachedFooter = "";
|
|
5
|
+
let loadPromise = null;
|
|
6
|
+
async function loadFromHub(baseUrl) {
|
|
7
|
+
try {
|
|
8
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/mcp/agent-context`;
|
|
9
|
+
const res = await fetch(url);
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
return;
|
|
12
|
+
const data = (await res.json());
|
|
13
|
+
if (typeof data.feedbackReporting === "string" && data.feedbackReporting.trim()) {
|
|
14
|
+
cachedFooter = data.feedbackReporting.trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* hub may be offline during local dev */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureLoaded(baseUrl) {
|
|
22
|
+
if (cachedFooter)
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
if (!loadPromise)
|
|
25
|
+
loadPromise = loadFromHub(baseUrl);
|
|
26
|
+
return loadPromise;
|
|
27
|
+
}
|
|
28
|
+
/** Async: resolves to MCP content block with optional feedback footer. */
|
|
29
|
+
export async function toolTextWithFeedback(baseUrl, body) {
|
|
30
|
+
await ensureLoaded(baseUrl);
|
|
31
|
+
const text = cachedFooter ? `${body}\n\n---\n${cachedFooter}` : body;
|
|
32
|
+
return { content: [{ type: "text", text }] };
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tymio/mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tymio hub MCP server (stdio) — exposes REST APIs as MCP tools via API key",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tymio-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
23
|
+
"zod": "^3.24.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.13.14",
|
|
27
|
+
"tsx": "^4.19.3",
|
|
28
|
+
"typescript": "^5.8.2"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|