@ojodotso/mcp-server 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/build/constants.d.ts +5 -0
  4. package/build/constants.d.ts.map +1 -0
  5. package/build/constants.js +7 -0
  6. package/build/index.d.ts +3 -0
  7. package/build/index.d.ts.map +1 -0
  8. package/build/index.js +20 -0
  9. package/build/prompts/index.d.ts +4 -0
  10. package/build/prompts/index.d.ts.map +1 -0
  11. package/build/prompts/index.js +61 -0
  12. package/build/server/index.d.ts +8 -0
  13. package/build/server/index.d.ts.map +1 -0
  14. package/build/server/index.js +16 -0
  15. package/build/server/ojo-client.d.ts +76 -0
  16. package/build/server/ojo-client.d.ts.map +1 -0
  17. package/build/server/ojo-client.js +146 -0
  18. package/build/server/preview.d.ts +12 -0
  19. package/build/server/preview.d.ts.map +1 -0
  20. package/build/server/preview.js +18 -0
  21. package/build/tools/handlers/image-handlers.d.ts +7 -0
  22. package/build/tools/handlers/image-handlers.d.ts.map +1 -0
  23. package/build/tools/handlers/image-handlers.js +49 -0
  24. package/build/tools/handlers/template-handlers.d.ts +291 -0
  25. package/build/tools/handlers/template-handlers.d.ts.map +1 -0
  26. package/build/tools/handlers/template-handlers.js +34 -0
  27. package/build/tools/index.d.ts +5 -0
  28. package/build/tools/index.d.ts.map +1 -0
  29. package/build/tools/index.js +31 -0
  30. package/build/tools/result.d.ts +6 -0
  31. package/build/tools/result.d.ts.map +1 -0
  32. package/build/tools/result.js +15 -0
  33. package/build/tools/schema.d.ts +34 -0
  34. package/build/tools/schema.d.ts.map +1 -0
  35. package/build/tools/schema.js +73 -0
  36. package/build/transports/stdio.d.ts +3 -0
  37. package/build/transports/stdio.d.ts.map +1 -0
  38. package/build/transports/stdio.js +7 -0
  39. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oJo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @ojodotso/mcp-server
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an
4
+ AI agent generate [oJo](https://ojo.so) images and manage templates through the
5
+ user's own oJo API key. Transport is local **stdio**; the API boundary is HTTP
6
+ to `https://api.ojo.so/v1` only.
7
+
8
+ ## Quick start
9
+
10
+ Create an API key at <https://ojo.so/dashboard/api>, then point your MCP client
11
+ at the package via `npx`:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "ojo": {
17
+ "command": "npx",
18
+ "args": ["-y", "@ojodotso/mcp-server"],
19
+ "env": { "OJO_API_KEY": "sk_..." }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ Works the same for Claude Desktop, Cursor, and Claude Code. For Claude Code:
26
+
27
+ ```bash
28
+ claude mcp add ojo -e OJO_API_KEY=sk_... -- npx -y @ojodotso/mcp-server
29
+ ```
30
+
31
+ ## Tools
32
+
33
+ | Tool | Purpose |
34
+ |------|---------|
35
+ | `generate_image_from_html` | Render plain HTML (with inline CSS) to a PNG; returns its public URL. |
36
+ | `generate_image_from_template` | Render an existing template, optionally overriding variables via `modify`. |
37
+ | `list_templates` | List the templates available to the account. |
38
+ | `get_template` | Fetch one template's decoded HTML and variable defaults. |
39
+ | `create_template` | Create a reusable template from plain HTML/Handlebars. |
40
+
41
+ Both generate tools accept `inspect: true` to also return a small downscaled
42
+ preview image, so the agent can see the render and iterate. Each image
43
+ generation costs 1 credit; creating a template does not.
44
+
45
+ ## Guiding prompt
46
+
47
+ `author_ojo_template` — teaches the Handlebars helper catalog, the **Template
48
+ Variables** (schema/defaults) vs **Modify Payload** (per-render data) model, and
49
+ viewport conventions.
50
+
51
+ ## Configuration
52
+
53
+ - `OJO_API_KEY` (required) — your oJo API key.
54
+ - `OJO_API_BASE_URL` (optional) — defaults to `https://api.ojo.so/v1`.
55
+
56
+ ## Develop (from the monorepo)
57
+
58
+ ```bash
59
+ pnpm --filter @ojodotso/mcp-server build
60
+ pnpm --filter @ojodotso/mcp-server test
61
+ pnpm --filter @ojodotso/mcp-server check
62
+ ```
@@ -0,0 +1,5 @@
1
+ export declare const OJO_API_BASE_URL = "https://api.ojo.so/v1";
2
+ export declare const HEADER_VIEWPORT_WIDTH = "x-viewport-width";
3
+ export declare const HEADER_VIEWPORT_HEIGHT = "x-viewport-height";
4
+ export declare const HEADER_TRANSPARENT_BACKGROUND = "x-transparent-background";
5
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAKxD,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,eAAO,MAAM,6BAA6B,6BAA6B,CAAC"}
@@ -0,0 +1,7 @@
1
+ export const OJO_API_BASE_URL = 'https://api.ojo.so/v1';
2
+ // Inlined from packages/libs (request-headers) so the published package carries
3
+ // no workspace dependencies. Keep in sync with
4
+ // packages/libs/src/constants/request-headers.ts.
5
+ export const HEADER_VIEWPORT_WIDTH = 'x-viewport-width';
6
+ export const HEADER_VIEWPORT_HEIGHT = 'x-viewport-height';
7
+ export const HEADER_TRANSPARENT_BACKGROUND = 'x-transparent-background';
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/build/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import dotenv from 'dotenv';
3
+ import { createServer } from './server/index.js';
4
+ import { OjoApiClient } from './server/ojo-client.js';
5
+ import { startStdio } from './transports/stdio.js';
6
+ dotenv.config();
7
+ async function main() {
8
+ const apiKey = process.env.OJO_API_KEY;
9
+ if (!apiKey) {
10
+ console.error('OJO_API_KEY is not set. Create an API key at https://ojo.so/dashboard/api and set OJO_API_KEY before starting the server.');
11
+ process.exit(1);
12
+ }
13
+ const client = new OjoApiClient(apiKey, process.env.OJO_API_BASE_URL);
14
+ const server = createServer(client);
15
+ await startStdio(server);
16
+ }
17
+ main().catch((error) => {
18
+ console.error('Fatal error starting the oJo MCP server:', error);
19
+ process.exit(1);
20
+ });
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** Register the v1 guiding prompt that teaches oJo template semantics. */
3
+ export declare function registerPrompts(server: McpServer): void;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkDzE,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAiBvD"}
@@ -0,0 +1,61 @@
1
+ // Handlebars helper catalog — keep in sync with
2
+ // packages/libs/src/template/handlebars-helpers.ts (and the public docs at
3
+ // apps/web/content/template-helpers.mdx). Drift here misleads the agent.
4
+ const GUIDE = `# Authoring oJo templates
5
+
6
+ You generate images from HTML. Two ways:
7
+ - **One-off:** \`generate_image_from_html\` with plain HTML + inline CSS.
8
+ - **Reusable:** \`create_template\` once, then \`generate_image_from_template\`
9
+ with a per-render **Modify Payload** that fills the placeholders.
10
+
11
+ ## Template Variables vs Modify Payload
12
+ - **Template Variables** = the *schema/defaults* you pass to \`create_template\`
13
+ (the \`variables\` object). They are the shape of a template's inputs.
14
+ - **Modify Payload** = the *data for one render*, passed as \`modify\` to
15
+ \`generate_image_from_template\`. Its keys override the variable defaults for
16
+ that single image. Use nested objects/arrays freely.
17
+
18
+ ## Placeholders
19
+ Author placeholders in the HTML with Handlebars: \`{{title}}\`, \`{{user.name}}\`,
20
+ \`{{#each items}}...{{/each}}\`, \`{{#if premium}}...{{/if}}\`.
21
+
22
+ ## Helper catalog (the only helpers available)
23
+ - \`{{json value}}\` — value as a JSON string
24
+ - \`{{array value}}\` — value as a JSON array string
25
+ - \`{{eq a b}}\` / \`{{gt a b}}\` / \`{{lt a b}}\` — boolean comparisons
26
+ - \`{{ternary cond yes no}}\` — pick yes/no by condition
27
+ - \`{{formatDate dateString "YYYY-MM-DD"}}\` — dayjs formatting
28
+ - \`{{isBefore date1 date2}}\` / \`{{isAfter date1 date2}}\` — date comparisons
29
+ - \`{{math a "+" b}}\` — arithmetic; operators: + - * / %
30
+ - \`{{round value decimals}}\` — fixed-decimal rounding
31
+ - \`{{inc value}}\` / \`{{dec value}}\` — +1 / -1
32
+ - \`{{concat a b c}}\` — concatenate strings
33
+ - \`{{formatCurrency amount "USD" 0 2}}\` — Intl currency (min/max digits optional)
34
+ - \`{{formatNumber number}}\` — Intl number formatting
35
+ - \`{{uppercase s}}\` / \`{{lowercase s}}\` / \`{{capitalize s}}\` — case helpers
36
+ - \`{{substring s start end}}\` — slice a string
37
+ - \`{{len arr}}\` — array length
38
+ - \`{{#times n}}...{{/times}}\` — repeat a block n times
39
+
40
+ ## Viewport & background
41
+ Default viewport is 1280×800. Pass \`viewportWidth\`/\`viewportHeight\` to change it,
42
+ and \`transparentBackground: true\` for a transparent (non-white) background.
43
+
44
+ ## Iterating
45
+ Each generated image costs 1 credit and returns a public \`url\`. Pass
46
+ \`inspect: true\` on a generate call to also get a small preview image back so you
47
+ can see the render and refine the HTML before finalizing.`;
48
+ /** Register the v1 guiding prompt that teaches oJo template semantics. */
49
+ export function registerPrompts(server) {
50
+ server.registerPrompt('author_ojo_template', {
51
+ title: 'Author an oJo template',
52
+ description: 'Guidance for writing oJo templates: the Handlebars helper catalog, Template Variables vs Modify Payload, viewport conventions, and the inspect→iterate loop.',
53
+ }, () => ({
54
+ messages: [
55
+ {
56
+ role: 'user',
57
+ content: { type: 'text', text: GUIDE },
58
+ },
59
+ ],
60
+ }));
61
+ }
@@ -0,0 +1,8 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { OjoApiClient } from './ojo-client.js';
3
+ /**
4
+ * Build a transport-agnostic McpServer wired to the given oJo API client.
5
+ * The transport (stdio for v1) is attached separately by the entrypoint.
6
+ */
7
+ export declare function createServer(client: OjoApiClient): McpServer;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CAQ5D"}
@@ -0,0 +1,16 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerTools } from '../tools/index.js';
3
+ import { registerPrompts } from '../prompts/index.js';
4
+ /**
5
+ * Build a transport-agnostic McpServer wired to the given oJo API client.
6
+ * The transport (stdio for v1) is attached separately by the entrypoint.
7
+ */
8
+ export function createServer(client) {
9
+ const server = new McpServer({
10
+ name: 'ojo',
11
+ version: '0.1.0',
12
+ });
13
+ registerTools(server, client);
14
+ registerPrompts(server);
15
+ return server;
16
+ }
@@ -0,0 +1,76 @@
1
+ export interface ViewportOptions {
2
+ viewportWidth?: number;
3
+ viewportHeight?: number;
4
+ transparentBackground?: boolean;
5
+ }
6
+ export interface GeneratedImage {
7
+ id: string;
8
+ imageUrl: string;
9
+ createdAt?: string;
10
+ }
11
+ export interface ListTemplatesParams {
12
+ page?: number;
13
+ pageSize?: number;
14
+ sort?: 'asc' | 'desc';
15
+ }
16
+ export interface TemplateSummary {
17
+ template_id: string;
18
+ name: string;
19
+ preview_url?: string;
20
+ created_at: string;
21
+ updated_at: string;
22
+ }
23
+ export interface TemplateList {
24
+ items: TemplateSummary[];
25
+ totalItems: number;
26
+ totalPages: number;
27
+ currentPage: number;
28
+ pageSize: number;
29
+ }
30
+ export interface TemplateDetail {
31
+ id: string;
32
+ name: string;
33
+ /** Decoded (plain) HTML — the API returns it base64-encoded; decoded here. */
34
+ html: string;
35
+ variables: unknown;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+ export interface CreatedTemplate {
40
+ id: string;
41
+ message?: string;
42
+ }
43
+ /** Error from the oJo API carrying the HTTP status and an agent-readable message. */
44
+ export declare class OjoApiError extends Error {
45
+ readonly status: number;
46
+ constructor(status: number, message: string);
47
+ }
48
+ /**
49
+ * Thin HTTP client over the public oJo API (`/v1`). Authenticates with the
50
+ * user's oJo API key via `Authorization: Bearer <key>`. Never imports internal
51
+ * workspace packages — the boundary is HTTP only.
52
+ */
53
+ export declare class OjoApiClient {
54
+ private readonly apiKey;
55
+ private readonly baseUrl;
56
+ constructor(apiKey: string, baseUrl?: string);
57
+ /**
58
+ * POST /image/html — render raw HTML. The server requires the Content-Type to
59
+ * be exactly `text/html` (a strict `!==` check), so it must not carry a charset.
60
+ */
61
+ createImageFromHtml(html: string, options?: ViewportOptions): Promise<GeneratedImage>;
62
+ /** POST /image/template — render an existing template, optionally overriding its variables. */
63
+ createImageFromTemplate(templateId: string, modify?: Record<string, unknown>, options?: ViewportOptions): Promise<GeneratedImage>;
64
+ /** GET /template — list the templates available to this account. */
65
+ listTemplates(params?: ListTemplatesParams): Promise<TemplateList>;
66
+ /** GET /template/:id — fetch one template; the API returns base64 html, decoded here. */
67
+ getTemplate(templateId: string): Promise<TemplateDetail>;
68
+ /** POST /template — create a template from plain HTML; the html is base64-encoded here. */
69
+ createTemplate(html: string, variables?: Record<string, unknown>): Promise<CreatedTemplate>;
70
+ private request;
71
+ private toError;
72
+ private describe;
73
+ }
74
+ /** The API may return a scheme-less hotlink (e.g. `ojousercontent.com/...`); make it absolute https. */
75
+ export declare function toAbsoluteUrl(url: string): string;
76
+ //# sourceMappingURL=ojo-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ojo-client.d.ts","sourceRoot":"","sources":["../../src/server/ojo-client.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,qBAAa,WAAY,SAAQ,KAAK;aAElB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;CAKlB;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,MAAyB;IAK9D;;;OAGG;IACG,mBAAmB,CACvB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC;IAmB1B,+FAA+F;IACzF,uBAAuB,CAC3B,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC;IAmB1B,oEAAoE;IAC9D,aAAa,CAAC,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC;IAa5E,yFAAyF;IACnF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAW9D,2FAA2F;IACrF,cAAc,CAClB,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,eAAe,CAAC;YAcb,OAAO;YA8BP,OAAO;IAWrB,OAAO,CAAC,QAAQ;CAcjB;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,146 @@
1
+ import { HEADER_TRANSPARENT_BACKGROUND, HEADER_VIEWPORT_HEIGHT, HEADER_VIEWPORT_WIDTH, OJO_API_BASE_URL, } from '../constants.js';
2
+ /** Error from the oJo API carrying the HTTP status and an agent-readable message. */
3
+ export class OjoApiError extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.name = 'OjoApiError';
9
+ }
10
+ }
11
+ /**
12
+ * Thin HTTP client over the public oJo API (`/v1`). Authenticates with the
13
+ * user's oJo API key via `Authorization: Bearer <key>`. Never imports internal
14
+ * workspace packages — the boundary is HTTP only.
15
+ */
16
+ export class OjoApiClient {
17
+ apiKey;
18
+ baseUrl;
19
+ constructor(apiKey, baseUrl = OJO_API_BASE_URL) {
20
+ this.apiKey = apiKey;
21
+ this.baseUrl = baseUrl;
22
+ }
23
+ /**
24
+ * POST /image/html — render raw HTML. The server requires the Content-Type to
25
+ * be exactly `text/html` (a strict `!==` check), so it must not carry a charset.
26
+ */
27
+ async createImageFromHtml(html, options = {}) {
28
+ const headers = { 'Content-Type': 'text/html' };
29
+ if (options.viewportWidth !== undefined) {
30
+ headers[HEADER_VIEWPORT_WIDTH] = String(options.viewportWidth);
31
+ }
32
+ if (options.viewportHeight !== undefined) {
33
+ headers[HEADER_VIEWPORT_HEIGHT] = String(options.viewportHeight);
34
+ }
35
+ if (options.transparentBackground) {
36
+ headers[HEADER_TRANSPARENT_BACKGROUND] = 'true';
37
+ }
38
+ const result = await this.request('/image/html', {
39
+ method: 'POST',
40
+ headers,
41
+ body: html,
42
+ });
43
+ return { ...result, imageUrl: toAbsoluteUrl(result.imageUrl) };
44
+ }
45
+ /** POST /image/template — render an existing template, optionally overriding its variables. */
46
+ async createImageFromTemplate(templateId, modify = {}, options = {}) {
47
+ const body = { templateId, modify };
48
+ if (options.viewportWidth !== undefined) {
49
+ body.viewportWidth = options.viewportWidth;
50
+ }
51
+ if (options.viewportHeight !== undefined) {
52
+ body.viewportHeight = options.viewportHeight;
53
+ }
54
+ if (options.transparentBackground !== undefined) {
55
+ body.transparentBackground = options.transparentBackground;
56
+ }
57
+ const result = await this.request('/image/template', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify(body),
61
+ });
62
+ return { ...result, imageUrl: toAbsoluteUrl(result.imageUrl) };
63
+ }
64
+ /** GET /template — list the templates available to this account. */
65
+ async listTemplates(params = {}) {
66
+ const query = new URLSearchParams();
67
+ if (params.page !== undefined)
68
+ query.set('page', String(params.page));
69
+ if (params.pageSize !== undefined) {
70
+ query.set('pageSize', String(params.pageSize));
71
+ }
72
+ if (params.sort !== undefined)
73
+ query.set('sort', params.sort);
74
+ const qs = query.toString();
75
+ return this.request(`/template${qs ? `?${qs}` : ''}`, {
76
+ method: 'GET',
77
+ });
78
+ }
79
+ /** GET /template/:id — fetch one template; the API returns base64 html, decoded here. */
80
+ async getTemplate(templateId) {
81
+ const record = await this.request(`/template/${encodeURIComponent(templateId)}`, { method: 'GET' });
82
+ return {
83
+ ...record,
84
+ html: Buffer.from(record.html, 'base64').toString('utf-8'),
85
+ };
86
+ }
87
+ /** POST /template — create a template from plain HTML; the html is base64-encoded here. */
88
+ async createTemplate(html, variables) {
89
+ const body = {
90
+ html: Buffer.from(html, 'utf-8').toString('base64'),
91
+ };
92
+ if (variables !== undefined) {
93
+ body.variables = variables;
94
+ }
95
+ return this.request('/template', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify(body),
99
+ });
100
+ }
101
+ async request(path, init) {
102
+ let response;
103
+ try {
104
+ response = await fetch(`${this.baseUrl}${path}`, {
105
+ method: init.method,
106
+ headers: { ...init.headers, Authorization: `Bearer ${this.apiKey}` },
107
+ body: init.body,
108
+ });
109
+ }
110
+ catch (cause) {
111
+ const detail = cause instanceof Error ? cause.message : String(cause);
112
+ throw new OjoApiError(0, `Could not reach the oJo API at ${this.baseUrl}: ${detail}`);
113
+ }
114
+ if (!response.ok) {
115
+ throw await this.toError(response);
116
+ }
117
+ const contentType = response.headers.get('content-type') ?? '';
118
+ if (contentType.includes('application/json')) {
119
+ return (await response.json());
120
+ }
121
+ return (await response.text());
122
+ }
123
+ async toError(response) {
124
+ const body = (await response.json().catch(() => null));
125
+ const serverMessage = body?.message ?? body?.error ?? response.statusText;
126
+ return new OjoApiError(response.status, this.describe(response.status, serverMessage));
127
+ }
128
+ describe(status, serverMessage) {
129
+ switch (status) {
130
+ case 401:
131
+ return 'Unauthorized — set OJO_API_KEY to a valid oJo API key (create one at https://ojo.so/dashboard/api).';
132
+ case 402:
133
+ return 'Out of credits — this oJo account has no remaining image credits.';
134
+ case 422:
135
+ return `The oJo API rejected the request: ${serverMessage}`;
136
+ case 429:
137
+ return 'Rate limited by the oJo API — slow down and retry shortly.';
138
+ default:
139
+ return `oJo API error ${status}: ${serverMessage}`;
140
+ }
141
+ }
142
+ }
143
+ /** The API may return a scheme-less hotlink (e.g. `ojousercontent.com/...`); make it absolute https. */
144
+ export function toAbsoluteUrl(url) {
145
+ return /^https?:\/\//i.test(url) ? url : `https://${url}`;
146
+ }
@@ -0,0 +1,12 @@
1
+ export interface ImagePreview {
2
+ data: string;
3
+ mimeType: string;
4
+ }
5
+ /**
6
+ * Fetch a generated image from its public URL and downscale it locally to a
7
+ * small webp so it can be returned as inline MCP image content without bloating
8
+ * the agent's context. The public hotlink needs no auth and costs no credit.
9
+ */
10
+ export declare function renderPreview(imageUrl: string): Promise<ImagePreview>;
11
+ export type RenderPreview = typeof renderPreview;
12
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/server/preview.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAW3E;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC"}
@@ -0,0 +1,18 @@
1
+ import sharp from 'sharp';
2
+ /**
3
+ * Fetch a generated image from its public URL and downscale it locally to a
4
+ * small webp so it can be returned as inline MCP image content without bloating
5
+ * the agent's context. The public hotlink needs no auth and costs no credit.
6
+ */
7
+ export async function renderPreview(imageUrl) {
8
+ const response = await fetch(imageUrl);
9
+ if (!response.ok) {
10
+ throw new Error(`Failed to fetch image for preview (${response.status})`);
11
+ }
12
+ const input = Buffer.from(await response.arrayBuffer());
13
+ const output = await sharp(input)
14
+ .resize({ width: 512, withoutEnlargement: true })
15
+ .webp({ quality: 80 })
16
+ .toBuffer();
17
+ return { data: output.toString('base64'), mimeType: 'image/webp' };
18
+ }
@@ -0,0 +1,7 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { OjoApiClient } from '../../server/ojo-client.js';
3
+ import { type RenderPreview } from '../../server/preview.js';
4
+ import type { GenerateImageFromHtmlArgs, GenerateImageFromTemplateArgs } from '../schema.js';
5
+ export declare function makeGenerateImageFromHtml(client: OjoApiClient, preview?: RenderPreview): (args: GenerateImageFromHtmlArgs) => Promise<CallToolResult>;
6
+ export declare function makeGenerateImageFromTemplate(client: OjoApiClient, preview?: RenderPreview): (args: GenerateImageFromTemplateArgs) => Promise<CallToolResult>;
7
+ //# sourceMappingURL=image-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-handlers.d.ts","sourceRoot":"","sources":["../../../src/tools/handlers/image-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAEV,YAAY,EACb,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EACV,yBAAyB,EACzB,6BAA6B,EAC9B,MAAM,cAAc,CAAC;AAGtB,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,aAA6B,IAExB,MAAM,yBAAyB,KAAG,OAAO,CAAC,cAAc,CAAC,CASxE;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,aAA6B,IAGpC,MAAM,6BAA6B,KAClC,OAAO,CAAC,cAAc,CAAC,CAa3B"}
@@ -0,0 +1,49 @@
1
+ import { renderPreview } from '../../server/preview.js';
2
+ import { toErrorResult } from '../result.js';
3
+ export function makeGenerateImageFromHtml(client, preview = renderPreview) {
4
+ return async (args) => {
5
+ try {
6
+ const { html, inspect, ...viewport } = args;
7
+ const image = await client.createImageFromHtml(html, viewport);
8
+ return buildImageResult(image, inspect ?? false, preview);
9
+ }
10
+ catch (error) {
11
+ return toErrorResult(error);
12
+ }
13
+ };
14
+ }
15
+ export function makeGenerateImageFromTemplate(client, preview = renderPreview) {
16
+ return async (args) => {
17
+ try {
18
+ const { templateId, modify, inspect, ...viewport } = args;
19
+ const image = await client.createImageFromTemplate(templateId, modify ?? {}, viewport);
20
+ return buildImageResult(image, inspect ?? false, preview);
21
+ }
22
+ catch (error) {
23
+ return toErrorResult(error);
24
+ }
25
+ };
26
+ }
27
+ async function buildImageResult(image, inspect, preview) {
28
+ const content = [
29
+ { type: 'text', text: formatImage(image) },
30
+ ];
31
+ if (inspect) {
32
+ try {
33
+ const { data, mimeType } = await preview(image.imageUrl);
34
+ content.push({ type: 'image', data, mimeType });
35
+ }
36
+ catch (error) {
37
+ const detail = error instanceof Error ? error.message : String(error);
38
+ content.push({ type: 'text', text: `(preview unavailable: ${detail})` });
39
+ }
40
+ }
41
+ return { content };
42
+ }
43
+ function formatImage(image) {
44
+ return [
45
+ 'Image generated successfully.',
46
+ `id: ${image.id}`,
47
+ `url: ${image.imageUrl}`,
48
+ ].join('\n');
49
+ }
@@ -0,0 +1,291 @@
1
+ import type { OjoApiClient } from '../../server/ojo-client.js';
2
+ import type { CreateTemplateArgs, GetTemplateArgs, ListTemplatesArgs } from '../schema.js';
3
+ export declare function makeListTemplates(client: OjoApiClient): (args: ListTemplatesArgs) => Promise<{
4
+ [x: string]: unknown;
5
+ content: ({
6
+ type: "text";
7
+ text: string;
8
+ annotations?: {
9
+ audience?: ("user" | "assistant")[] | undefined;
10
+ priority?: number | undefined;
11
+ lastModified?: string | undefined;
12
+ } | undefined;
13
+ _meta?: {
14
+ [x: string]: unknown;
15
+ } | undefined;
16
+ } | {
17
+ type: "image";
18
+ data: string;
19
+ mimeType: string;
20
+ annotations?: {
21
+ audience?: ("user" | "assistant")[] | undefined;
22
+ priority?: number | undefined;
23
+ lastModified?: string | undefined;
24
+ } | undefined;
25
+ _meta?: {
26
+ [x: string]: unknown;
27
+ } | undefined;
28
+ } | {
29
+ type: "audio";
30
+ data: string;
31
+ mimeType: string;
32
+ annotations?: {
33
+ audience?: ("user" | "assistant")[] | undefined;
34
+ priority?: number | undefined;
35
+ lastModified?: string | undefined;
36
+ } | undefined;
37
+ _meta?: {
38
+ [x: string]: unknown;
39
+ } | undefined;
40
+ } | {
41
+ uri: string;
42
+ name: string;
43
+ type: "resource_link";
44
+ description?: string | undefined;
45
+ mimeType?: string | undefined;
46
+ annotations?: {
47
+ audience?: ("user" | "assistant")[] | undefined;
48
+ priority?: number | undefined;
49
+ lastModified?: string | undefined;
50
+ } | undefined;
51
+ _meta?: {
52
+ [x: string]: unknown;
53
+ } | undefined;
54
+ icons?: {
55
+ src: string;
56
+ mimeType?: string | undefined;
57
+ sizes?: string[] | undefined;
58
+ theme?: "light" | "dark" | undefined;
59
+ }[] | undefined;
60
+ title?: string | undefined;
61
+ } | {
62
+ type: "resource";
63
+ resource: {
64
+ uri: string;
65
+ text: string;
66
+ mimeType?: string | undefined;
67
+ _meta?: {
68
+ [x: string]: unknown;
69
+ } | undefined;
70
+ } | {
71
+ uri: string;
72
+ blob: string;
73
+ mimeType?: string | undefined;
74
+ _meta?: {
75
+ [x: string]: unknown;
76
+ } | undefined;
77
+ };
78
+ annotations?: {
79
+ audience?: ("user" | "assistant")[] | undefined;
80
+ priority?: number | undefined;
81
+ lastModified?: string | undefined;
82
+ } | undefined;
83
+ _meta?: {
84
+ [x: string]: unknown;
85
+ } | undefined;
86
+ })[];
87
+ _meta?: {
88
+ [x: string]: unknown;
89
+ progressToken?: string | number | undefined;
90
+ "io.modelcontextprotocol/related-task"?: {
91
+ taskId: string;
92
+ } | undefined;
93
+ } | undefined;
94
+ structuredContent?: {
95
+ [x: string]: unknown;
96
+ } | undefined;
97
+ isError?: boolean | undefined;
98
+ }>;
99
+ export declare function makeGetTemplate(client: OjoApiClient): (args: GetTemplateArgs) => Promise<{
100
+ [x: string]: unknown;
101
+ content: ({
102
+ type: "text";
103
+ text: string;
104
+ annotations?: {
105
+ audience?: ("user" | "assistant")[] | undefined;
106
+ priority?: number | undefined;
107
+ lastModified?: string | undefined;
108
+ } | undefined;
109
+ _meta?: {
110
+ [x: string]: unknown;
111
+ } | undefined;
112
+ } | {
113
+ type: "image";
114
+ data: string;
115
+ mimeType: string;
116
+ annotations?: {
117
+ audience?: ("user" | "assistant")[] | undefined;
118
+ priority?: number | undefined;
119
+ lastModified?: string | undefined;
120
+ } | undefined;
121
+ _meta?: {
122
+ [x: string]: unknown;
123
+ } | undefined;
124
+ } | {
125
+ type: "audio";
126
+ data: string;
127
+ mimeType: string;
128
+ annotations?: {
129
+ audience?: ("user" | "assistant")[] | undefined;
130
+ priority?: number | undefined;
131
+ lastModified?: string | undefined;
132
+ } | undefined;
133
+ _meta?: {
134
+ [x: string]: unknown;
135
+ } | undefined;
136
+ } | {
137
+ uri: string;
138
+ name: string;
139
+ type: "resource_link";
140
+ description?: string | undefined;
141
+ mimeType?: string | undefined;
142
+ annotations?: {
143
+ audience?: ("user" | "assistant")[] | undefined;
144
+ priority?: number | undefined;
145
+ lastModified?: string | undefined;
146
+ } | undefined;
147
+ _meta?: {
148
+ [x: string]: unknown;
149
+ } | undefined;
150
+ icons?: {
151
+ src: string;
152
+ mimeType?: string | undefined;
153
+ sizes?: string[] | undefined;
154
+ theme?: "light" | "dark" | undefined;
155
+ }[] | undefined;
156
+ title?: string | undefined;
157
+ } | {
158
+ type: "resource";
159
+ resource: {
160
+ uri: string;
161
+ text: string;
162
+ mimeType?: string | undefined;
163
+ _meta?: {
164
+ [x: string]: unknown;
165
+ } | undefined;
166
+ } | {
167
+ uri: string;
168
+ blob: string;
169
+ mimeType?: string | undefined;
170
+ _meta?: {
171
+ [x: string]: unknown;
172
+ } | undefined;
173
+ };
174
+ annotations?: {
175
+ audience?: ("user" | "assistant")[] | undefined;
176
+ priority?: number | undefined;
177
+ lastModified?: string | undefined;
178
+ } | undefined;
179
+ _meta?: {
180
+ [x: string]: unknown;
181
+ } | undefined;
182
+ })[];
183
+ _meta?: {
184
+ [x: string]: unknown;
185
+ progressToken?: string | number | undefined;
186
+ "io.modelcontextprotocol/related-task"?: {
187
+ taskId: string;
188
+ } | undefined;
189
+ } | undefined;
190
+ structuredContent?: {
191
+ [x: string]: unknown;
192
+ } | undefined;
193
+ isError?: boolean | undefined;
194
+ }>;
195
+ export declare function makeCreateTemplate(client: OjoApiClient): (args: CreateTemplateArgs) => Promise<{
196
+ [x: string]: unknown;
197
+ content: ({
198
+ type: "text";
199
+ text: string;
200
+ annotations?: {
201
+ audience?: ("user" | "assistant")[] | undefined;
202
+ priority?: number | undefined;
203
+ lastModified?: string | undefined;
204
+ } | undefined;
205
+ _meta?: {
206
+ [x: string]: unknown;
207
+ } | undefined;
208
+ } | {
209
+ type: "image";
210
+ data: string;
211
+ mimeType: string;
212
+ annotations?: {
213
+ audience?: ("user" | "assistant")[] | undefined;
214
+ priority?: number | undefined;
215
+ lastModified?: string | undefined;
216
+ } | undefined;
217
+ _meta?: {
218
+ [x: string]: unknown;
219
+ } | undefined;
220
+ } | {
221
+ type: "audio";
222
+ data: string;
223
+ mimeType: string;
224
+ annotations?: {
225
+ audience?: ("user" | "assistant")[] | undefined;
226
+ priority?: number | undefined;
227
+ lastModified?: string | undefined;
228
+ } | undefined;
229
+ _meta?: {
230
+ [x: string]: unknown;
231
+ } | undefined;
232
+ } | {
233
+ uri: string;
234
+ name: string;
235
+ type: "resource_link";
236
+ description?: string | undefined;
237
+ mimeType?: string | undefined;
238
+ annotations?: {
239
+ audience?: ("user" | "assistant")[] | undefined;
240
+ priority?: number | undefined;
241
+ lastModified?: string | undefined;
242
+ } | undefined;
243
+ _meta?: {
244
+ [x: string]: unknown;
245
+ } | undefined;
246
+ icons?: {
247
+ src: string;
248
+ mimeType?: string | undefined;
249
+ sizes?: string[] | undefined;
250
+ theme?: "light" | "dark" | undefined;
251
+ }[] | undefined;
252
+ title?: string | undefined;
253
+ } | {
254
+ type: "resource";
255
+ resource: {
256
+ uri: string;
257
+ text: string;
258
+ mimeType?: string | undefined;
259
+ _meta?: {
260
+ [x: string]: unknown;
261
+ } | undefined;
262
+ } | {
263
+ uri: string;
264
+ blob: string;
265
+ mimeType?: string | undefined;
266
+ _meta?: {
267
+ [x: string]: unknown;
268
+ } | undefined;
269
+ };
270
+ annotations?: {
271
+ audience?: ("user" | "assistant")[] | undefined;
272
+ priority?: number | undefined;
273
+ lastModified?: string | undefined;
274
+ } | undefined;
275
+ _meta?: {
276
+ [x: string]: unknown;
277
+ } | undefined;
278
+ })[];
279
+ _meta?: {
280
+ [x: string]: unknown;
281
+ progressToken?: string | number | undefined;
282
+ "io.modelcontextprotocol/related-task"?: {
283
+ taskId: string;
284
+ } | undefined;
285
+ } | undefined;
286
+ structuredContent?: {
287
+ [x: string]: unknown;
288
+ } | undefined;
289
+ isError?: boolean | undefined;
290
+ }>;
291
+ //# sourceMappingURL=template-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-handlers.d.ts","sourceRoot":"","sources":["../../../src/tools/handlers/template-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAGtB,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,IACtC,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQtC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,IACpC,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQpC;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,IACvC,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQvC"}
@@ -0,0 +1,34 @@
1
+ import { textResult, toErrorResult } from '../result.js';
2
+ export function makeListTemplates(client) {
3
+ return async (args) => {
4
+ try {
5
+ const result = await client.listTemplates(args);
6
+ return textResult(JSON.stringify(result, null, 2));
7
+ }
8
+ catch (error) {
9
+ return toErrorResult(error);
10
+ }
11
+ };
12
+ }
13
+ export function makeGetTemplate(client) {
14
+ return async (args) => {
15
+ try {
16
+ const result = await client.getTemplate(args.templateId);
17
+ return textResult(JSON.stringify(result, null, 2));
18
+ }
19
+ catch (error) {
20
+ return toErrorResult(error);
21
+ }
22
+ };
23
+ }
24
+ export function makeCreateTemplate(client) {
25
+ return async (args) => {
26
+ try {
27
+ const result = await client.createTemplate(args.html, args.variables);
28
+ return textResult(JSON.stringify(result, null, 2));
29
+ }
30
+ catch (error) {
31
+ return toErrorResult(error);
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { OjoApiClient } from '../server/ojo-client.js';
3
+ /** Register the Slice 0 + Slice 1 tool surface on an McpServer. */
4
+ export declare function registerTools(server: McpServer, client: OjoApiClient): void;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAkB5D,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAsD3E"}
@@ -0,0 +1,31 @@
1
+ import { createTemplateSchema, generateImageFromHtmlSchema, generateImageFromTemplateSchema, getTemplateSchema, listTemplatesSchema, } from './schema.js';
2
+ import { makeGenerateImageFromHtml, makeGenerateImageFromTemplate, } from './handlers/image-handlers.js';
3
+ import { makeCreateTemplate, makeGetTemplate, makeListTemplates, } from './handlers/template-handlers.js';
4
+ /** Register the Slice 0 + Slice 1 tool surface on an McpServer. */
5
+ export function registerTools(server, client) {
6
+ server.registerTool('generate_image_from_html', {
7
+ title: 'Generate image from HTML',
8
+ description: 'Render plain HTML (with inline CSS) into a PNG and return its public URL. Costs 1 credit.',
9
+ inputSchema: generateImageFromHtmlSchema,
10
+ }, makeGenerateImageFromHtml(client));
11
+ server.registerTool('generate_image_from_template', {
12
+ title: 'Generate image from template',
13
+ description: 'Render an existing oJo template into a PNG, optionally overriding its variables via `modify`. Costs 1 credit.',
14
+ inputSchema: generateImageFromTemplateSchema,
15
+ }, makeGenerateImageFromTemplate(client));
16
+ server.registerTool('list_templates', {
17
+ title: 'List templates',
18
+ description: 'List the oJo templates available to this account.',
19
+ inputSchema: listTemplatesSchema,
20
+ }, makeListTemplates(client));
21
+ server.registerTool('get_template', {
22
+ title: 'Get template',
23
+ description: 'Fetch one oJo template by id, returning its decoded HTML and variable defaults.',
24
+ inputSchema: getTemplateSchema,
25
+ }, makeGetTemplate(client));
26
+ server.registerTool('create_template', {
27
+ title: 'Create template',
28
+ description: 'Create a reusable oJo template from plain HTML/Handlebars (with optional variable defaults). Does not cost a credit.',
29
+ inputSchema: createTemplateSchema,
30
+ }, makeCreateTemplate(client));
31
+ }
@@ -0,0 +1,6 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare function textResult(text: string): CallToolResult;
3
+ export declare function errorResult(message: string): CallToolResult;
4
+ /** Map any thrown value to an MCP error result the agent can read. */
5
+ export declare function toErrorResult(error: unknown): CallToolResult;
6
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/tools/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGzE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAEvD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAE3D;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAM5D"}
@@ -0,0 +1,15 @@
1
+ import { OjoApiError } from '../server/ojo-client.js';
2
+ export function textResult(text) {
3
+ return { content: [{ type: 'text', text }] };
4
+ }
5
+ export function errorResult(message) {
6
+ return { content: [{ type: 'text', text: message }], isError: true };
7
+ }
8
+ /** Map any thrown value to an MCP error result the agent can read. */
9
+ export function toErrorResult(error) {
10
+ if (error instanceof OjoApiError) {
11
+ return errorResult(error.message);
12
+ }
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ return errorResult(`Unexpected error: ${message}`);
15
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export declare const generateImageFromHtmlSchema: {
3
+ html: z.ZodString;
4
+ viewportWidth: z.ZodOptional<z.ZodNumber>;
5
+ viewportHeight: z.ZodOptional<z.ZodNumber>;
6
+ transparentBackground: z.ZodOptional<z.ZodBoolean>;
7
+ inspect: z.ZodOptional<z.ZodBoolean>;
8
+ };
9
+ export declare const generateImageFromTemplateSchema: {
10
+ templateId: z.ZodString;
11
+ modify: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
12
+ viewportWidth: z.ZodOptional<z.ZodNumber>;
13
+ viewportHeight: z.ZodOptional<z.ZodNumber>;
14
+ transparentBackground: z.ZodOptional<z.ZodBoolean>;
15
+ inspect: z.ZodOptional<z.ZodBoolean>;
16
+ };
17
+ export declare const listTemplatesSchema: {
18
+ page: z.ZodOptional<z.ZodNumber>;
19
+ pageSize: z.ZodOptional<z.ZodNumber>;
20
+ sort: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
21
+ };
22
+ export declare const getTemplateSchema: {
23
+ templateId: z.ZodString;
24
+ };
25
+ export declare const createTemplateSchema: {
26
+ html: z.ZodString;
27
+ variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
28
+ };
29
+ export type GenerateImageFromHtmlArgs = z.infer<z.ZodObject<typeof generateImageFromHtmlSchema>>;
30
+ export type GenerateImageFromTemplateArgs = z.infer<z.ZodObject<typeof generateImageFromTemplateSchema>>;
31
+ export type ListTemplatesArgs = z.infer<z.ZodObject<typeof listTemplatesSchema>>;
32
+ export type GetTemplateArgs = z.infer<z.ZodObject<typeof getTemplateSchema>>;
33
+ export type CreateTemplateArgs = z.infer<z.ZodObject<typeof createTemplateSchema>>;
34
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/tools/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA4BxB,eAAO,MAAM,2BAA2B;;;;;;CAUf,CAAC;AAE1B,eAAO,MAAM,+BAA+B;;;;;;;CAcnB,CAAC;AAE1B,eAAO,MAAM,mBAAmB;;;;CAiBP,CAAC;AAE1B,eAAO,MAAM,iBAAiB;;CAEL,CAAC;AAE1B,eAAO,MAAM,oBAAoB;;;CAYR,CAAC;AAE1B,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAC7C,CAAC,CAAC,SAAS,CAAC,OAAO,2BAA2B,CAAC,CAChD,CAAC;AACF,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CACjD,CAAC,CAAC,SAAS,CAAC,OAAO,+BAA+B,CAAC,CACpD,CAAC;AACF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC;AACjF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC;AAC7E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CACtC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CACzC,CAAC"}
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ const viewportWidth = z
3
+ .number()
4
+ .int()
5
+ .positive()
6
+ .optional()
7
+ .describe('Viewport width in pixels (default 1280).');
8
+ const viewportHeight = z
9
+ .number()
10
+ .int()
11
+ .positive()
12
+ .optional()
13
+ .describe('Viewport height in pixels (default 800).');
14
+ const transparentBackground = z
15
+ .boolean()
16
+ .optional()
17
+ .describe('Render with a transparent background instead of white.');
18
+ const inspect = z
19
+ .boolean()
20
+ .optional()
21
+ .describe('When true, also return a small downscaled preview image of the result so you can see it and iterate.');
22
+ export const generateImageFromHtmlSchema = {
23
+ html: z
24
+ .string()
25
+ .describe('Plain HTML (with inline CSS) to render into an image. Do NOT base64-encode it.'),
26
+ viewportWidth,
27
+ viewportHeight,
28
+ transparentBackground,
29
+ inspect,
30
+ };
31
+ export const generateImageFromTemplateSchema = {
32
+ templateId: z
33
+ .string()
34
+ .describe('Id of an existing oJo template (from list_templates).'),
35
+ modify: z
36
+ .record(z.any())
37
+ .optional()
38
+ .describe('Modify Payload: per-render values that override the template variable defaults (merged over the defaults).'),
39
+ viewportWidth,
40
+ viewportHeight,
41
+ transparentBackground,
42
+ inspect,
43
+ };
44
+ export const listTemplatesSchema = {
45
+ page: z
46
+ .number()
47
+ .int()
48
+ .positive()
49
+ .optional()
50
+ .describe('1-based page number (default 1).'),
51
+ pageSize: z
52
+ .number()
53
+ .int()
54
+ .positive()
55
+ .optional()
56
+ .describe('Number of templates per page (default 10).'),
57
+ sort: z
58
+ .enum(['asc', 'desc'])
59
+ .optional()
60
+ .describe('Sort by creation time, ascending or descending (default desc).'),
61
+ };
62
+ export const getTemplateSchema = {
63
+ templateId: z.string().describe('Id of the template to fetch.'),
64
+ };
65
+ export const createTemplateSchema = {
66
+ html: z
67
+ .string()
68
+ .describe('Plain HTML/Handlebars for the template, using {{variable}} placeholders. Do NOT base64-encode it.'),
69
+ variables: z
70
+ .record(z.any())
71
+ .optional()
72
+ .describe('Template Variables: the default values/shape for the template placeholders (the schema, not per-render data).'),
73
+ };
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function startStdio(server: McpServer): Promise<void>;
3
+ //# sourceMappingURL=stdio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/transports/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjE"}
@@ -0,0 +1,7 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ export async function startStdio(server) {
3
+ const transport = new StdioServerTransport();
4
+ await server.connect(transport);
5
+ // Log to stderr so it never corrupts the stdout JSON-RPC stream.
6
+ console.error('oJo MCP server connected via stdio.');
7
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@ojodotso/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for oJo — let AI agents generate images from HTML and templates via your oJo API key.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "ojo",
9
+ "screenshot",
10
+ "image-generation",
11
+ "html-to-image",
12
+ "ai",
13
+ "agent"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "oJo",
17
+ "homepage": "https://github.com/ojodotso/ojo/tree/master/apps/mcp-server#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ojodotso/ojo.git",
21
+ "directory": "apps/mcp-server"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/ojodotso/ojo/issues"
25
+ },
26
+ "type": "module",
27
+ "bin": {
28
+ "ojo-mcp": "build/index.js"
29
+ },
30
+ "files": [
31
+ "build",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.25.2",
42
+ "dotenv": "^16.4.5",
43
+ "sharp": "^0.34.5",
44
+ "zod": "^3.22.4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.16.4",
48
+ "tsx": "^4.22.4",
49
+ "typescript": "^5.3.3",
50
+ "vitest": "^3.2.6"
51
+ },
52
+ "scripts": {
53
+ "dev": "tsx watch ./src/index.ts",
54
+ "build": "tsc",
55
+ "start": "node build/index.js",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "lint": "eslint . --max-warnings 0",
59
+ "depcheck": "depcheck",
60
+ "check": "tsc --noEmit -p tsconfig.test.json"
61
+ }
62
+ }