@tymio/mcp-server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const GUIDANCE_FILE = "TYMIO_MCP_CLI_AGENT_GUIDANCE.md";
5
+ function loadAgentGuidanceMarkdown() {
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const packaged = path.join(here, "..", GUIDANCE_FILE);
8
+ try {
9
+ if (existsSync(packaged))
10
+ return readFileSync(packaged, "utf8");
11
+ }
12
+ catch {
13
+ /* fall through */
14
+ }
15
+ return [
16
+ "# Tymio MCP CLI",
17
+ "",
18
+ "The guidance file `TYMIO_MCP_CLI_AGENT_GUIDANCE.md` was not found next to this package (expected beside `dist/`). Reinstall `@tymio/mcp-server` or run from the repository checkout.",
19
+ "",
20
+ "## Critical (read before advising the user)",
21
+ "",
22
+ "- **No per-user MCP API key** exists in Tymio Settings, Profile, or Account. Do not tell users to copy one from the UI.",
23
+ "- **OAuth:** remote MCP URL `https://tymio.app/mcp` (or your host) in the IDE, or install `@tymio/mcp-server` and run `tymio-mcp login` for stdio **without** `DRD_API_KEY`/`API_KEY` on that process unless you want API-key mode.",
24
+ "- **`DRD_API_KEY`/`API_KEY`** on the stdio process is the **server deployment** automation secret, not a personal user key.",
25
+ "",
26
+ "After fixing the install: `tymio-mcp instructions`",
27
+ ].join("\n");
28
+ }
29
+ /** Short usage (stderr) — keep in sync with guidance file for agents. */
30
+ export const HELP_SUMMARY = `Tymio MCP CLI (@tymio/mcp-server)
31
+
32
+ Commands:
33
+ tymio-mcp Start stdio MCP (default: OAuth → hosted Tymio MCP)
34
+ tymio-mcp login [url] Sign in with Google (browser). Saves tokens locally.
35
+ tymio-mcp logout Delete saved OAuth client + tokens
36
+ tymio-mcp instructions Full setup text for humans & coding agents (print this)
37
+ tymio-mcp help This summary
38
+
39
+ Environment:
40
+ TYMIO_MCP_URL Hosted MCP URL (default https://tymio.app/mcp)
41
+ TYMIO_OAUTH_PORT Loopback port for login callback (default 19876)
42
+ TYMIO_MCP_QUIET If set, suppress stderr hints when starting stdio
43
+ DRD_API_KEY / API_KEY If set → API-key REST tool bridge (subset), not OAuth proxy
44
+ DRD_API_BASE_URL Hub origin for API-key bridge (default https://tymio.app)
45
+
46
+ Critical for agents: There is NO MCP API key in Tymio user Settings — use OAuth (remote /mcp URL or tymio-mcp login).
47
+ Tip: Run tymio-mcp instructions for the full Markdown guide, Cursor JSON, and troubleshooting.
48
+ `;
49
+ /**
50
+ * Long-form instructions for coding agents and operators (Markdown).
51
+ * Loaded from `TYMIO_MCP_CLI_AGENT_GUIDANCE.md` beside the installed package / dist output.
52
+ */
53
+ export const AGENT_INSTRUCTIONS = loadAgentGuidanceMarkdown();
@@ -0,0 +1,6 @@
1
+ /** XDG-style config directory for OAuth tokens and dynamic client registration. */
2
+ export declare function getTymioConfigDir(): string;
3
+ export declare function ensureConfigDir(): string;
4
+ export declare function defaultMcpUrl(): URL;
5
+ /** Loopback redirect port for OAuth `login` (must stay stable across runs for dynamic client registration). */
6
+ export declare function defaultOAuthRedirectUrl(): URL;
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /** XDG-style config directory for OAuth tokens and dynamic client registration. */
5
+ export function getTymioConfigDir() {
6
+ const base = process.env.XDG_CONFIG_HOME ?? (process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support") : path.join(os.homedir(), ".config"));
7
+ return path.join(base, "tymio-mcp");
8
+ }
9
+ export function ensureConfigDir() {
10
+ const dir = getTymioConfigDir();
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ return dir;
13
+ }
14
+ export function defaultMcpUrl() {
15
+ const raw = (process.env.TYMIO_MCP_URL ?? "https://tymio.app/mcp").trim();
16
+ const fallback = "https://tymio.app/mcp";
17
+ const base = raw.length > 0 ? raw : fallback;
18
+ const trimmed = base.replace(/\/+$/, "");
19
+ const withMcp = trimmed.endsWith("/mcp") ? trimmed : `${trimmed}/mcp`;
20
+ return new URL(withMcp);
21
+ }
22
+ /** Loopback redirect port for OAuth `login` (must stay stable across runs for dynamic client registration). */
23
+ export function defaultOAuthRedirectUrl() {
24
+ const raw = process.env.TYMIO_OAUTH_PORT;
25
+ let port = 19876;
26
+ if (raw !== undefined && raw.trim() !== "") {
27
+ const n = Number(raw);
28
+ if (Number.isInteger(n) && n >= 1 && n <= 65535)
29
+ port = n;
30
+ }
31
+ return new URL(`http://127.0.0.1:${port}/callback`);
32
+ }
@@ -0,0 +1,27 @@
1
+ import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ /**
4
+ * Persists MCP OAuth dynamic registration + tokens for the Tymio hub Streamable HTTP endpoint.
5
+ */
6
+ export declare class FileOAuthProvider implements OAuthClientProvider {
7
+ private readonly dir;
8
+ private readonly redirect;
9
+ private codeVerifierValue?;
10
+ private oauthState?;
11
+ constructor(redirectUrl: URL);
12
+ get redirectUrl(): URL;
13
+ get clientMetadata(): OAuthClientMetadata;
14
+ clientInformation(): OAuthClientInformationMixed | undefined;
15
+ saveClientInformation(clientInformation: OAuthClientInformationMixed): void;
16
+ tokens(): OAuthTokens | undefined;
17
+ saveTokens(tokens: OAuthTokens): void;
18
+ saveCodeVerifier(codeVerifier: string): void;
19
+ codeVerifier(): string;
20
+ state(): Promise<string>;
21
+ clearLoginSession(): void;
22
+ redirectToAuthorization(authorizationUrl: URL): void;
23
+ saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
24
+ discoveryState(): Promise<OAuthDiscoveryState | undefined>;
25
+ invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void>;
26
+ }
27
+ export declare function removeAllOAuthFiles(): void;
@@ -0,0 +1,127 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { ensureConfigDir, getTymioConfigDir } from "./configPaths.js";
6
+ const CLIENT_FILE = "oauth-client.json";
7
+ const TOKENS_FILE = "oauth-tokens.json";
8
+ const DISCOVERY_FILE = "oauth-discovery.json";
9
+ function readJson(file) {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(file, "utf8"));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ function writeJson(file, data) {
18
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf8");
19
+ }
20
+ function openUrlInBrowser(url) {
21
+ if (process.platform === "darwin") {
22
+ spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
23
+ }
24
+ else if (process.platform === "win32") {
25
+ spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
26
+ }
27
+ else {
28
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
29
+ }
30
+ }
31
+ /**
32
+ * Persists MCP OAuth dynamic registration + tokens for the Tymio hub Streamable HTTP endpoint.
33
+ */
34
+ export class FileOAuthProvider {
35
+ dir;
36
+ redirect;
37
+ codeVerifierValue;
38
+ oauthState;
39
+ constructor(redirectUrl) {
40
+ this.dir = ensureConfigDir();
41
+ this.redirect = redirectUrl;
42
+ }
43
+ get redirectUrl() {
44
+ return this.redirect;
45
+ }
46
+ get clientMetadata() {
47
+ return {
48
+ redirect_uris: [this.redirect.toString()],
49
+ client_name: "Tymio MCP CLI",
50
+ grant_types: ["authorization_code", "refresh_token"],
51
+ response_types: ["code"],
52
+ token_endpoint_auth_method: "none",
53
+ scope: "mcp:tools"
54
+ };
55
+ }
56
+ clientInformation() {
57
+ return readJson(path.join(this.dir, CLIENT_FILE));
58
+ }
59
+ saveClientInformation(clientInformation) {
60
+ writeJson(path.join(this.dir, CLIENT_FILE), clientInformation);
61
+ }
62
+ tokens() {
63
+ return readJson(path.join(this.dir, TOKENS_FILE));
64
+ }
65
+ saveTokens(tokens) {
66
+ writeJson(path.join(this.dir, TOKENS_FILE), tokens);
67
+ }
68
+ saveCodeVerifier(codeVerifier) {
69
+ this.codeVerifierValue = codeVerifier;
70
+ }
71
+ codeVerifier() {
72
+ if (!this.codeVerifierValue)
73
+ throw new Error("Missing PKCE code verifier");
74
+ return this.codeVerifierValue;
75
+ }
76
+ async state() {
77
+ if (!this.oauthState) {
78
+ this.oauthState = randomBytes(16).toString("hex");
79
+ }
80
+ return this.oauthState;
81
+ }
82
+ clearLoginSession() {
83
+ this.codeVerifierValue = undefined;
84
+ this.oauthState = undefined;
85
+ }
86
+ redirectToAuthorization(authorizationUrl) {
87
+ process.stderr.write(`Opening browser to sign in to Tymio…\n${authorizationUrl.toString()}\n`);
88
+ openUrlInBrowser(authorizationUrl.toString());
89
+ }
90
+ async saveDiscoveryState(state) {
91
+ writeJson(path.join(this.dir, DISCOVERY_FILE), state);
92
+ }
93
+ async discoveryState() {
94
+ return readJson(path.join(this.dir, DISCOVERY_FILE));
95
+ }
96
+ async invalidateCredentials(scope) {
97
+ const base = getTymioConfigDir();
98
+ const rm = (f) => {
99
+ try {
100
+ fs.unlinkSync(path.join(base, f));
101
+ }
102
+ catch {
103
+ /* ignore */
104
+ }
105
+ };
106
+ if (scope === "all" || scope === "tokens")
107
+ rm(TOKENS_FILE);
108
+ if (scope === "all" || scope === "client")
109
+ rm(CLIENT_FILE);
110
+ if (scope === "all" || scope === "discovery")
111
+ rm(DISCOVERY_FILE);
112
+ if (scope === "all" || scope === "verifier") {
113
+ this.codeVerifierValue = undefined;
114
+ }
115
+ }
116
+ }
117
+ export function removeAllOAuthFiles() {
118
+ const base = getTymioConfigDir();
119
+ for (const f of [CLIENT_FILE, TOKENS_FILE, DISCOVERY_FILE]) {
120
+ try {
121
+ fs.unlinkSync(path.join(base, f));
122
+ }
123
+ catch {
124
+ /* ignore */
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Stdio MCP server that proxies to the hosted Tymio Streamable HTTP MCP endpoint with OAuth tokens on disk.
3
+ */
4
+ export declare function runHubOAuthStdio(mcpUrl?: URL): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { defaultMcpUrl, defaultOAuthRedirectUrl } from "./configPaths.js";
9
+ import { FileOAuthProvider } from "./fileOAuthProvider.js";
10
+ import { AGENT_INSTRUCTIONS } from "./cliMessages.js";
11
+ import { writeStdioStartupHint } from "./stdioHints.js";
12
+ function pkgVersion() {
13
+ try {
14
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
15
+ return JSON.parse(raw).version;
16
+ }
17
+ catch {
18
+ return "1.0.0";
19
+ }
20
+ }
21
+ const passthroughArgs = z.object({}).passthrough();
22
+ /**
23
+ * Stdio MCP server that proxies to the hosted Tymio Streamable HTTP MCP endpoint with OAuth tokens on disk.
24
+ */
25
+ export async function runHubOAuthStdio(mcpUrl = defaultMcpUrl()) {
26
+ writeStdioStartupHint("oauth");
27
+ const redirectUrl = defaultOAuthRedirectUrl();
28
+ const provider = new FileOAuthProvider(redirectUrl);
29
+ const transport = new StreamableHTTPClientTransport(mcpUrl, { authProvider: provider });
30
+ const client = new Client({ name: "@tymio/mcp-server", version: pkgVersion() }, { capabilities: {} });
31
+ try {
32
+ await client.connect(transport);
33
+ }
34
+ catch (e) {
35
+ if (e instanceof UnauthorizedError) {
36
+ process.stderr.write("Tymio MCP: not signed in or session expired.\n Run: tymio-mcp login (there is no MCP API key in Tymio user Settings)\n Or: tymio-mcp instructions (full setup for agents)\n API-key mode: set DRD_API_KEY (server deployment secret), not a UI setting.\n");
37
+ process.exit(1);
38
+ }
39
+ throw e;
40
+ }
41
+ const { tools } = await client.listTools();
42
+ const server = new McpServer({ name: "tymio-hub", version: pkgVersion() }, { instructions: AGENT_INSTRUCTIONS });
43
+ for (const tool of tools) {
44
+ const name = tool.name;
45
+ server.registerTool(name, {
46
+ title: tool.title,
47
+ description: tool.description ?? "",
48
+ inputSchema: passthroughArgs
49
+ }, async (args) => {
50
+ const result = await client.callTool({
51
+ name,
52
+ arguments: args ?? {}
53
+ });
54
+ return result;
55
+ });
56
+ }
57
+ const stdio = new StdioServerTransport();
58
+ await server.connect(stdio);
59
+ }
package/dist/index.js CHANGED
@@ -1,283 +1,6 @@
1
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) => {
2
+ import { runCli } from "./cli.js";
3
+ runCli(process.argv).catch((err) => {
281
4
  console.error(err);
282
5
  process.exit(1);
283
6
  });
@@ -0,0 +1 @@
1
+ export declare function runLoginCommand(mcpUrl?: URL): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import { defaultMcpUrl, defaultOAuthRedirectUrl } from "./configPaths.js";
3
+ import { FileOAuthProvider } from "./fileOAuthProvider.js";
4
+ import { startOAuthCallbackServer } from "./oauthCallbackServer.js";
5
+ export async function runLoginCommand(mcpUrl = defaultMcpUrl()) {
6
+ const redirectUrl = defaultOAuthRedirectUrl();
7
+ const provider = new FileOAuthProvider(redirectUrl);
8
+ const { waitForCode, close } = await startOAuthCallbackServer(redirectUrl, provider);
9
+ try {
10
+ let result = await auth(provider, { serverUrl: mcpUrl });
11
+ if (result === "REDIRECT") {
12
+ const code = await waitForCode;
13
+ result = await auth(provider, { serverUrl: mcpUrl, authorizationCode: code });
14
+ }
15
+ else {
16
+ void waitForCode.catch(() => {
17
+ /* already authorized; browser callback not used */
18
+ });
19
+ }
20
+ if (result !== "AUTHORIZED") {
21
+ throw new Error(`Unexpected auth result: ${result}`);
22
+ }
23
+ provider.clearLoginSession();
24
+ process.stderr.write("Tymio MCP login succeeded. You can run your editor MCP client (tymio-mcp).\n");
25
+ }
26
+ finally {
27
+ close();
28
+ provider.clearLoginSession();
29
+ }
30
+ }
@@ -0,0 +1,10 @@
1
+ import type { FileOAuthProvider } from "./fileOAuthProvider.js";
2
+ export interface OAuthCallbackHandle {
3
+ waitForCode: Promise<string>;
4
+ close: () => void;
5
+ }
6
+ /**
7
+ * Listens on the host/port of `redirectUrl` and resolves with the authorization `code`
8
+ * once the browser hits the redirect URI. Resolves after the socket is accepting connections.
9
+ */
10
+ export declare function startOAuthCallbackServer(redirectUrl: URL, provider: FileOAuthProvider): Promise<OAuthCallbackHandle>;