@takuhon/core 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3697,20 +3697,69 @@ declare class ConflictError extends StorageError {
3697
3697
  * snapshot always renders the same markup, so both rendering surfaces (the
3698
3698
  * static HTML export and the React profile) and their tests stay in lockstep.
3699
3699
  *
3700
+ * The card paints an **opaque background rectangle** first, so it stays legible
3701
+ * wherever it is embedded — including a GitHub README served through the Camo
3702
+ * image proxy, where no surrounding page styles apply and a transparent card
3703
+ * would render invisibly on a dark theme. The colour set is supplied by a
3704
+ * {@link Palette}: {@link renderActivitySvg} defaults to {@link LIGHT_PALETTE},
3705
+ * and a transport (a Worker route, the CLI build) can pass {@link DARK_PALETTE}
3706
+ * to serve a dark variant. Both palettes are plain data, so the render stays
3707
+ * deterministic.
3708
+ *
3700
3709
  * Every snapshot-derived string (language names, dates) is XML-escaped before
3701
3710
  * it reaches the markup: language names come from an external API and are
3702
3711
  * treated as untrusted. An empty snapshot (no metric fields) renders to `''`
3703
3712
  * so callers can omit the section with a truthiness check.
3704
3713
  */
3705
3714
 
3715
+ /**
3716
+ * Colour set for an activity card. Kept as plain data (no CSS variables, no
3717
+ * `currentColor`) so the rendered SVG is fully self-contained and legible when
3718
+ * served as an image through Camo, where no page styles apply.
3719
+ */
3720
+ interface Palette {
3721
+ /** Fill of the opaque background rectangle behind the whole card. */
3722
+ background: string;
3723
+ /** Primary text (language legend, coding-time figure). */
3724
+ text: string;
3725
+ /** Secondary text (captions, rank score, "Last synced"). */
3726
+ muted: string;
3727
+ /** Accent fill (the rank badge disc). */
3728
+ accent: string;
3729
+ /**
3730
+ * Order-based palette for the language bar and its legend chips; entries are
3731
+ * assigned to the top languages in order, and anything past the end (or the
3732
+ * folded "Other" segment) uses {@link Palette.otherColor}.
3733
+ */
3734
+ languageColors: readonly string[];
3735
+ /** Colour of the folded "Other" language segment. */
3736
+ otherColor: string;
3737
+ /** Five-step contribution heat ramp, lowest (zero) first. */
3738
+ heatColors: readonly string[];
3739
+ }
3740
+ /** Default palette: dark text on an opaque white card. */
3741
+ declare const LIGHT_PALETTE: Palette;
3742
+ /**
3743
+ * Dark variant: light text on an opaque dark card (tuned to read on a GitHub
3744
+ * dark-theme README). The heat ramp stays in the card's blue family rather than
3745
+ * borrowing GitHub's green, so light and dark cards look like the same design.
3746
+ */
3747
+ declare const DARK_PALETTE: Palette;
3748
+ /** Options accepted by {@link renderActivitySvg}. */
3749
+ interface RenderActivitySvgOptions {
3750
+ /** Colour set to render with. Defaults to {@link LIGHT_PALETTE}. */
3751
+ palette?: Palette;
3752
+ }
3706
3753
  /**
3707
3754
  * Render the activity snapshot as a self-contained `<svg>` string, or `''`
3708
3755
  * when the snapshot carries no metric data (so callers can omit the section).
3709
3756
  * Sections render independently — whatever the snapshot holds is shown, the
3710
3757
  * rest is left out — and `lastSyncedAt`'s date is always stamped in the footer
3711
- * so staleness stays visible.
3758
+ * so staleness stays visible. Colours come from `options.palette`
3759
+ * ({@link LIGHT_PALETTE} by default); the card always paints an opaque
3760
+ * background so it stays legible when embedded as an image.
3712
3761
  */
3713
- declare function renderActivitySvg(snapshot: ActivitySnapshot): string;
3762
+ declare function renderActivitySvg(snapshot: ActivitySnapshot, options?: RenderActivitySvgOptions): string;
3714
3763
 
3715
3764
  /**
3716
3765
  * Résumé / CV projection of a profile.
@@ -3801,6 +3850,115 @@ interface CvDocument {
3801
3850
  */
3802
3851
  declare function deriveCv(localized: LocalizedTakuhon): CvDocument;
3803
3852
 
3853
+ /**
3854
+ * MCP (Model Context Protocol) projection of a profile.
3855
+ *
3856
+ * This module describes how a takuhon profile is exposed to an AI agent as a
3857
+ * set of read-only MCP tools and resources, and how each one is answered from a
3858
+ * profile document — *without* committing to a transport (stdio / HTTP) or to
3859
+ * any MCP SDK.
3860
+ *
3861
+ * The split mirrors the rest of core: deciding *what* to expose and *what it
3862
+ * returns* is a pure, deterministic projection over the canonical document
3863
+ * (exactly like {@link generateJsonLd}), so it lives here in `@takuhon/core` and
3864
+ * stays runtime-free (spec §2.3). The transport, JSON-RPC framing, and server
3865
+ * lifecycle are runtime-aware; they live in `@takuhon/mcp` and the CLI /
3866
+ * Cloudflare adapters, which call the pure executors below.
3867
+ *
3868
+ * Everything here is read-only. No tool or resource touches the admin surface,
3869
+ * and every answer passes through {@link applyPublicPrivacyFilter}, so an MCP
3870
+ * client sees exactly what `GET /api/profile`, `GET /api/jsonld`, `GET
3871
+ * /api/schema`, and `GET /takuhon.json` already expose — no more.
3872
+ *
3873
+ * The catalog ({@link MCP_TOOLS} / {@link MCP_RESOURCES}) is plain data with no
3874
+ * SDK dependency, so the `@takuhon/mcp` wiring can register it against the
3875
+ * official `@modelcontextprotocol/sdk` and later be swapped to a hand-rolled
3876
+ * dispatcher without touching this file.
3877
+ */
3878
+
3879
+ /**
3880
+ * Profile sections a `get_section` call may request: the content-bearing keys
3881
+ * of a locale-resolved profile (the spec §6 data sections). `settings`,
3882
+ * `meta`, `schemaVersion`, and `resolvedLocale` are intentionally excluded —
3883
+ * they are configuration / bookkeeping, not profile content an agent reads.
3884
+ */
3885
+ declare const MCP_PROFILE_SECTIONS: readonly ["profile", "links", "careers", "projects", "skills", "certifications", "memberships", "volunteering", "honors", "education", "publications", "languages", "courses", "patents", "testScores", "recommendations", "contact"];
3886
+ /** One of the {@link MCP_PROFILE_SECTIONS} a `get_section` call accepts. */
3887
+ type McpProfileSection = (typeof MCP_PROFILE_SECTIONS)[number];
3888
+ /** A JSON Schema (draft 2020-12) object describing a tool's input arguments. */
3889
+ type McpInputSchema = Readonly<Record<string, unknown>>;
3890
+ /** A read-only MCP tool definition — plain data, no SDK dependency. */
3891
+ interface McpToolDefinition {
3892
+ /** Stable tool id used in `tools/call`. */
3893
+ name: string;
3894
+ /** Human-readable title for display. */
3895
+ title: string;
3896
+ /** What the tool returns; shown to the agent when it chooses a tool. */
3897
+ description: string;
3898
+ /** JSON Schema for the tool's `arguments` object. */
3899
+ inputSchema: McpInputSchema;
3900
+ }
3901
+ /** A read-only MCP resource definition — plain data, no SDK dependency. */
3902
+ interface McpResourceDefinition {
3903
+ /** Stable resource URI used in `resources/read`. */
3904
+ uri: string;
3905
+ name: string;
3906
+ title: string;
3907
+ description: string;
3908
+ /** MIME type of the resource contents. */
3909
+ mimeType: string;
3910
+ }
3911
+ /** Result of {@link executeMcpTool}: the structured value the tool produced. */
3912
+ interface McpToolResult {
3913
+ /**
3914
+ * The JSON value the tool produced — already normalized, locale-resolved, and
3915
+ * privacy-filtered. The transport layer serializes this to MCP wire content.
3916
+ */
3917
+ data: unknown;
3918
+ /** Locale the result was resolved at, when the tool resolved one. */
3919
+ resolvedLocale?: string;
3920
+ }
3921
+ /** Result of {@link readMcpResource}: one resource's contents. */
3922
+ interface McpResourceResult {
3923
+ uri: string;
3924
+ mimeType: string;
3925
+ /** The JSON value of the resource. The transport layer serializes it. */
3926
+ data: unknown;
3927
+ }
3928
+ /**
3929
+ * Thrown by {@link executeMcpTool} / {@link readMcpResource} for an unknown
3930
+ * tool/resource name or invalid arguments. The transport layer maps it to the
3931
+ * appropriate MCP error (a JSON-RPC error for a malformed request, or an
3932
+ * `isError` tool result for a bad argument) — core stays transport-agnostic.
3933
+ */
3934
+ declare class McpRequestError extends Error {
3935
+ constructor(message: string, options?: {
3936
+ cause?: unknown;
3937
+ });
3938
+ }
3939
+ /** The read-only tools an MCP client may call against the profile. */
3940
+ declare const MCP_TOOLS: readonly McpToolDefinition[];
3941
+ /** The read-only resources an MCP client may read. */
3942
+ declare const MCP_RESOURCES: readonly McpResourceDefinition[];
3943
+ /**
3944
+ * Execute a read-only MCP tool against a profile document.
3945
+ *
3946
+ * `profile` is the raw validated {@link Takuhon} (the transport layer loaded it
3947
+ * from KV / a file). Locale-aware tools normalize, locale-resolve, and
3948
+ * privacy-filter it exactly as `GET /api/profile` does, then project the
3949
+ * requested view. Pure and deterministic — no clock, no randomness, no I/O.
3950
+ *
3951
+ * @throws {McpRequestError} for an unknown tool name or invalid arguments.
3952
+ */
3953
+ declare function executeMcpTool(name: string, args: Readonly<Record<string, unknown>>, profile: Takuhon): McpToolResult;
3954
+ /**
3955
+ * Read a read-only MCP resource from a profile document. Pure and
3956
+ * deterministic.
3957
+ *
3958
+ * @throws {McpRequestError} for an unknown resource URI.
3959
+ */
3960
+ declare function readMcpResource(uri: string, profile: Takuhon): McpResourceResult;
3961
+
3804
3962
  /**
3805
3963
  * Image-asset helpers for uploaded media (avatars, project images): magic-byte
3806
3964
  * type detection, header-only dimension / frame reading, and metadata stripping
@@ -3901,4 +4059,4 @@ declare function stripImageMetadata(bytes: Uint8Array, mime: AcceptedImageMime):
3901
4059
  */
3902
4060
  declare const SCHEMA_VERSION = "0.5.0";
3903
4061
 
3904
- export { ACCEPTED_IMAGE_MIME_TYPES, type AcceptedImageMime, type ActivitySettings, type ActivitySnapshot, type ActivityStorage, type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, type CodingTime, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type ContributionCalendar, type ContributionDay, type Course, type CvDocument, type CvHeader, type CvSection, type CvSectionKind, type Education, type ExportOptions, type ExportedTakuhon, type Honor, IMAGE_EXTENSIONS, type ImageInfo, ImportError, type Iso3166Alpha2, type IsoDateTime, type Language, type LanguageBreakdown, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, MAX_IMAGE_BYTES, MAX_IMAGE_DIMENSION, MAX_IMAGE_FRAMES, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Patent, type PatentStatus, type Profile, type Project, type Publication, RANK_FULL_CODING_HOURS, RANK_FULL_CONTRIBUTIONS, RANK_TIER_THRESHOLDS, type RankInput, type RankTier, type RankTierLabel, type Recommendation, type RecommendationAuthor, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, computeLanguagePercentages, deriveCv, deriveRankTier, detectImageMime, exportTakuhon, formatCodingTime, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, isActivitySnapshot, migrateTakuhon, migrations, normalize, readImageInfo, renderActivitySvg, resolveLocale, schema, stripImageMetadata, validate };
4062
+ export { ACCEPTED_IMAGE_MIME_TYPES, type AcceptedImageMime, type ActivitySettings, type ActivitySnapshot, type ActivityStorage, type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, type CodingTime, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type ContributionCalendar, type ContributionDay, type Course, type CvDocument, type CvHeader, type CvSection, type CvSectionKind, DARK_PALETTE, type Education, type ExportOptions, type ExportedTakuhon, type Honor, IMAGE_EXTENSIONS, type ImageInfo, ImportError, type Iso3166Alpha2, type IsoDateTime, LIGHT_PALETTE, type Language, type LanguageBreakdown, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, MAX_IMAGE_BYTES, MAX_IMAGE_DIMENSION, MAX_IMAGE_FRAMES, MCP_PROFILE_SECTIONS, MCP_RESOURCES, MCP_TOOLS, type McpInputSchema, type McpProfileSection, McpRequestError, type McpResourceDefinition, type McpResourceResult, type McpToolDefinition, type McpToolResult, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Palette, type Patent, type PatentStatus, type Profile, type Project, type Publication, RANK_FULL_CODING_HOURS, RANK_FULL_CONTRIBUTIONS, RANK_TIER_THRESHOLDS, type RankInput, type RankTier, type RankTierLabel, type Recommendation, type RecommendationAuthor, type RenderActivitySvgOptions, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, computeLanguagePercentages, deriveCv, deriveRankTier, detectImageMime, executeMcpTool, exportTakuhon, formatCodingTime, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, isActivitySnapshot, migrateTakuhon, migrations, normalize, readImageInfo, readMcpResource, renderActivitySvg, resolveLocale, schema, stripImageMetadata, validate };
package/dist/index.js CHANGED
@@ -11413,18 +11413,30 @@ function deriveRankTier(input) {
11413
11413
  var WIDTH = 520;
11414
11414
  var PAD = 16;
11415
11415
  var INNER = WIDTH - PAD * 2;
11416
- var TEXT_COLOR = "#1a1a1a";
11417
- var MUTED_COLOR = "#666666";
11418
- var ACCENT_COLOR = "#0b5fff";
11419
11416
  var FONT_FAMILY = "system-ui,-apple-system,Segoe UI,Roboto,sans-serif";
11420
- var LANGUAGE_COLORS = ["#0b5fff", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2"];
11421
- var OTHER_COLOR = "#9ca3af";
11422
11417
  var MAX_LANGUAGES = 6;
11423
- var HEAT_COLORS = ["#ebedf0", "#cce0ff", "#99c2ff", "#4d8aff", "#0b5fff"];
11424
11418
  var CELL = 7;
11425
11419
  var CELL_PITCH = 9;
11426
11420
  var CALENDAR_ROWS = 7;
11427
11421
  var MAX_CALENDAR_DAYS = 53 * CALENDAR_ROWS;
11422
+ var LIGHT_PALETTE = {
11423
+ background: "#ffffff",
11424
+ text: "#1a1a1a",
11425
+ muted: "#666666",
11426
+ accent: "#0b5fff",
11427
+ languageColors: ["#0b5fff", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2"],
11428
+ otherColor: "#9ca3af",
11429
+ heatColors: ["#ebedf0", "#cce0ff", "#99c2ff", "#4d8aff", "#0b5fff"]
11430
+ };
11431
+ var DARK_PALETTE = {
11432
+ background: "#0d1117",
11433
+ text: "#e6edf3",
11434
+ muted: "#9198a1",
11435
+ accent: "#4493f8",
11436
+ languageColors: ["#4493f8", "#a371f7", "#3fb950", "#d29922", "#f85149", "#39c5cf"],
11437
+ otherColor: "#6e7681",
11438
+ heatColors: ["#161b22", "#0a2c5e", "#11468f", "#1f6fdb", "#4493f8"]
11439
+ };
11428
11440
  function escapeXml(value) {
11429
11441
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11430
11442
  }
@@ -11441,23 +11453,23 @@ function text(x, y, content, opts) {
11441
11453
  const anchor = opts.anchor !== void 0 ? ` text-anchor="${opts.anchor}"` : "";
11442
11454
  return `<text x="${String(x)}" y="${String(y)}" font-family="${FONT_FAMILY}" font-size="${String(opts.size)}" fill="${opts.fill}"${weight}${anchor}>${escapeXml(content)}</text>`;
11443
11455
  }
11444
- function caption(x, y, label) {
11445
- return text(x, y, label, { size: 12, fill: MUTED_COLOR, weight: 600 });
11456
+ function caption(x, y, label, fill) {
11457
+ return text(x, y, label, { size: 12, fill, weight: 600 });
11446
11458
  }
11447
11459
  function rect(x, y, w, h, fill, rx = 0) {
11448
11460
  const corner = rx > 0 ? ` rx="${String(rx)}"` : "";
11449
11461
  return `<rect x="${String(round2(x))}" y="${String(y)}" width="${String(round2(w))}" height="${String(h)}" fill="${fill}"${corner}/>`;
11450
11462
  }
11451
- function languageSegments(languages) {
11463
+ function languageSegments(languages, palette) {
11452
11464
  const segments = languages.slice(0, MAX_LANGUAGES).map((l, i) => ({
11453
11465
  name: l.name,
11454
11466
  percent: l.percent,
11455
- color: LANGUAGE_COLORS[i] ?? OTHER_COLOR
11467
+ color: palette.languageColors[i] ?? palette.otherColor
11456
11468
  }));
11457
11469
  const rest = languages.slice(MAX_LANGUAGES);
11458
11470
  if (rest.length > 0) {
11459
11471
  const percent = round2(rest.reduce((sum, l) => sum + l.percent, 0));
11460
- segments.push({ name: "Other", percent, color: OTHER_COLOR });
11472
+ segments.push({ name: "Other", percent, color: palette.otherColor });
11461
11473
  }
11462
11474
  return segments.filter((s) => s.percent > 0);
11463
11475
  }
@@ -11465,7 +11477,8 @@ function heatLevel(count, max) {
11465
11477
  if (count <= 0) return 0;
11466
11478
  return Math.min(4, Math.max(1, Math.ceil(count / max * 4)));
11467
11479
  }
11468
- function renderActivitySvg(snapshot) {
11480
+ function renderActivitySvg(snapshot, options = {}) {
11481
+ const palette = options.palette ?? LIGHT_PALETTE;
11469
11482
  const hasLanguages = snapshot.languages !== void 0 && snapshot.languages.length > 0;
11470
11483
  const hasContributions = snapshot.contributions !== void 0 && snapshot.contributions.days.length > 0;
11471
11484
  const hasCodingTime = snapshot.codingTime !== void 0;
@@ -11474,9 +11487,9 @@ function renderActivitySvg(snapshot) {
11474
11487
  const parts = [];
11475
11488
  let y = PAD;
11476
11489
  if (hasLanguages && snapshot.languages) {
11477
- const segments = languageSegments(snapshot.languages);
11490
+ const segments = languageSegments(snapshot.languages, palette);
11478
11491
  const total = segments.reduce((sum, s) => sum + s.percent, 0);
11479
- parts.push(caption(PAD, y + 11, "Languages"));
11492
+ parts.push(caption(PAD, y + 11, "Languages", palette.muted));
11480
11493
  y += 22;
11481
11494
  let x = PAD;
11482
11495
  for (const segment of segments) {
@@ -11493,7 +11506,7 @@ function renderActivitySvg(snapshot) {
11493
11506
  parts.push(
11494
11507
  text(cx + 16, cy + 9, `${segment.name} ${String(segment.percent)}%`, {
11495
11508
  size: 12,
11496
- fill: TEXT_COLOR
11509
+ fill: palette.text
11497
11510
  })
11498
11511
  );
11499
11512
  });
@@ -11503,13 +11516,18 @@ function renderActivitySvg(snapshot) {
11503
11516
  const days = snapshot.contributions.days.slice(-MAX_CALENDAR_DAYS);
11504
11517
  const max = days.reduce((m, d) => Math.max(m, d.count), 1);
11505
11518
  parts.push(
11506
- caption(PAD, y + 11, `Contributions \xB7 ${groupDigits(snapshot.contributions.total)}`)
11519
+ caption(
11520
+ PAD,
11521
+ y + 11,
11522
+ `Contributions \xB7 ${groupDigits(snapshot.contributions.total)}`,
11523
+ palette.muted
11524
+ )
11507
11525
  );
11508
11526
  y += 22;
11509
11527
  days.forEach((day, i) => {
11510
11528
  const column = Math.floor(i / CALENDAR_ROWS);
11511
11529
  const row = i % CALENDAR_ROWS;
11512
- const fill = HEAT_COLORS[heatLevel(day.count, max)] ?? HEAT_COLORS[0];
11530
+ const fill = palette.heatColors[heatLevel(day.count, max)] ?? palette.heatColors[0];
11513
11531
  parts.push(rect(PAD + column * CELL_PITCH, y + row * CELL_PITCH, CELL, CELL, fill, 1.5));
11514
11532
  });
11515
11533
  y += CALENDAR_ROWS * CELL_PITCH - (CELL_PITCH - CELL) + 12;
@@ -11518,21 +11536,21 @@ function renderActivitySvg(snapshot) {
11518
11536
  const rankX = PAD + INNER - 120;
11519
11537
  if (hasCodingTime && snapshot.codingTime) {
11520
11538
  const t = snapshot.codingTime;
11521
- parts.push(caption(PAD, y + 11, "Coding time"));
11539
+ parts.push(caption(PAD, y + 11, "Coding time", palette.muted));
11522
11540
  parts.push(
11523
11541
  text(PAD, y + 38, `${groupDigits(t.hours)}h ${String(t.minutes)}m`, {
11524
11542
  size: 22,
11525
- fill: TEXT_COLOR,
11543
+ fill: palette.text,
11526
11544
  weight: 700
11527
11545
  })
11528
11546
  );
11529
11547
  }
11530
11548
  if (hasRank && snapshot.rank) {
11531
11549
  const rank = snapshot.rank;
11532
- parts.push(caption(rankX, y + 11, "Rank"));
11550
+ parts.push(caption(rankX, y + 11, "Rank", palette.muted));
11533
11551
  const centerY = y + 32;
11534
11552
  parts.push(
11535
- `<circle cx="${String(rankX + 14)}" cy="${String(centerY)}" r="14" fill="${ACCENT_COLOR}"/>`
11553
+ `<circle cx="${String(rankX + 14)}" cy="${String(centerY)}" r="14" fill="${palette.accent}"/>`
11536
11554
  );
11537
11555
  parts.push(
11538
11556
  `<text x="${String(rankX + 14)}" y="${String(centerY)}" font-family="${FONT_FAMILY}" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle" dominant-baseline="central">${escapeXml(rank.tier)}</text>`
@@ -11540,7 +11558,7 @@ function renderActivitySvg(snapshot) {
11540
11558
  parts.push(
11541
11559
  text(rankX + 36, centerY + 4, `score ${String(rank.score)}`, {
11542
11560
  size: 12,
11543
- fill: MUTED_COLOR
11561
+ fill: palette.muted
11544
11562
  })
11545
11563
  );
11546
11564
  }
@@ -11549,13 +11567,14 @@ function renderActivitySvg(snapshot) {
11549
11567
  parts.push(
11550
11568
  text(WIDTH - PAD, y + 11, `Last synced ${snapshot.lastSyncedAt.slice(0, 10)}`, {
11551
11569
  size: 11,
11552
- fill: MUTED_COLOR,
11570
+ fill: palette.muted,
11553
11571
  anchor: "end"
11554
11572
  })
11555
11573
  );
11556
11574
  y += 22;
11557
11575
  const height = y + PAD - 12;
11558
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(WIDTH)}" height="${String(height)}" viewBox="0 0 ${String(WIDTH)} ${String(height)}" role="img" aria-label="Developer activity"><title>Developer activity</title>${parts.join("")}</svg>`;
11576
+ const background = rect(0, 0, WIDTH, height, palette.background);
11577
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(WIDTH)}" height="${String(height)}" viewBox="0 0 ${String(WIDTH)} ${String(height)}" role="img" aria-label="Developer activity"><title>Developer activity</title>${background}${parts.join("")}</svg>`;
11559
11578
  }
11560
11579
 
11561
11580
  // src/cv.ts
@@ -11587,6 +11606,161 @@ function deriveCv(localized) {
11587
11606
  };
11588
11607
  }
11589
11608
 
11609
+ // src/mcp.ts
11610
+ var MCP_PROFILE_SECTIONS = [
11611
+ "profile",
11612
+ "links",
11613
+ "careers",
11614
+ "projects",
11615
+ "skills",
11616
+ "certifications",
11617
+ "memberships",
11618
+ "volunteering",
11619
+ "honors",
11620
+ "education",
11621
+ "publications",
11622
+ "languages",
11623
+ "courses",
11624
+ "patents",
11625
+ "testScores",
11626
+ "recommendations",
11627
+ "contact"
11628
+ ];
11629
+ var McpRequestError = class extends Error {
11630
+ constructor(message, options) {
11631
+ super(message, options);
11632
+ this.name = "McpRequestError";
11633
+ }
11634
+ };
11635
+ var LANG_PROPERTY = {
11636
+ type: "string",
11637
+ description: 'BCP-47 locale tag (e.g. "ja", "en"). Resolves to the requested locale, falling back to the profile default when omitted or unavailable.'
11638
+ };
11639
+ var MCP_TOOLS = [
11640
+ {
11641
+ name: "get_profile",
11642
+ title: "Get profile",
11643
+ description: "Return the owner's full public profile, resolved to one locale and privacy-filtered (identical to the public `GET /api/profile`).",
11644
+ inputSchema: {
11645
+ type: "object",
11646
+ properties: { lang: LANG_PROPERTY },
11647
+ additionalProperties: false
11648
+ }
11649
+ },
11650
+ {
11651
+ name: "get_section",
11652
+ title: "Get profile section",
11653
+ description: "Return a single section of the public profile (e.g. careers, education, skills), resolved to one locale and privacy-filtered.",
11654
+ inputSchema: {
11655
+ type: "object",
11656
+ properties: {
11657
+ section: {
11658
+ type: "string",
11659
+ enum: [...MCP_PROFILE_SECTIONS],
11660
+ description: "Which profile section to return."
11661
+ },
11662
+ lang: LANG_PROPERTY
11663
+ },
11664
+ required: ["section"],
11665
+ additionalProperties: false
11666
+ }
11667
+ },
11668
+ {
11669
+ name: "get_jsonld",
11670
+ title: "Get JSON-LD",
11671
+ description: "Return the Schema.org JSON-LD (a `ProfilePage` wrapping a `Person`) for the profile, resolved to one locale (identical to `GET /api/jsonld`).",
11672
+ inputSchema: {
11673
+ type: "object",
11674
+ properties: { lang: LANG_PROPERTY },
11675
+ additionalProperties: false
11676
+ }
11677
+ },
11678
+ {
11679
+ name: "list_locales",
11680
+ title: "List locales",
11681
+ description: "List the locales this profile is available in, plus the default \u2014 useful before calling another tool with a `lang` argument.",
11682
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
11683
+ }
11684
+ ];
11685
+ var MCP_RESOURCES = [
11686
+ {
11687
+ uri: "takuhon://profile",
11688
+ name: "profile",
11689
+ title: "Canonical profile (takuhon.json)",
11690
+ description: "The full canonical takuhon.json, privacy-filtered, with every locale retained (identical to `GET /takuhon.json`).",
11691
+ mimeType: "application/json"
11692
+ },
11693
+ {
11694
+ uri: "takuhon://schema",
11695
+ name: "schema",
11696
+ title: "Takuhon JSON Schema",
11697
+ description: "The public JSON Schema contract the profile conforms to (identical to `GET /api/schema`). Lets an agent understand the document shape.",
11698
+ mimeType: "application/json"
11699
+ }
11700
+ ];
11701
+ function executeMcpTool(name, args, profile) {
11702
+ switch (name) {
11703
+ case "get_profile": {
11704
+ const localized = publicView(profile, readLang(args));
11705
+ return { data: localized, resolvedLocale: localized.resolvedLocale };
11706
+ }
11707
+ case "get_section": {
11708
+ const section = readSection(args);
11709
+ const localized = publicView(profile, readLang(args));
11710
+ return { data: localized[section], resolvedLocale: localized.resolvedLocale };
11711
+ }
11712
+ case "get_jsonld": {
11713
+ const localized = publicView(profile, readLang(args));
11714
+ return { data: generateJsonLd(localized), resolvedLocale: localized.resolvedLocale };
11715
+ }
11716
+ case "list_locales":
11717
+ return {
11718
+ data: {
11719
+ defaultLocale: profile.settings.defaultLocale,
11720
+ availableLocales: profile.settings.availableLocales,
11721
+ ...profile.settings.fallbackLocale !== void 0 ? { fallbackLocale: profile.settings.fallbackLocale } : {}
11722
+ }
11723
+ };
11724
+ default:
11725
+ throw new McpRequestError(`Unknown MCP tool: ${name}`);
11726
+ }
11727
+ }
11728
+ function readMcpResource(uri, profile) {
11729
+ switch (uri) {
11730
+ case "takuhon://profile":
11731
+ return {
11732
+ uri,
11733
+ mimeType: "application/json",
11734
+ data: applyPublicPrivacyFilter(profile)
11735
+ };
11736
+ case "takuhon://schema":
11737
+ return { uri, mimeType: "application/json", data: schema };
11738
+ default:
11739
+ throw new McpRequestError(`Unknown MCP resource: ${uri}`);
11740
+ }
11741
+ }
11742
+ function publicView(profile, lang) {
11743
+ return applyPublicPrivacyFilter(resolveLocale(normalize(profile), lang));
11744
+ }
11745
+ function readLang(args) {
11746
+ const lang = args.lang;
11747
+ if (lang === void 0) return void 0;
11748
+ if (typeof lang !== "string") {
11749
+ throw new McpRequestError("`lang` must be a string (a BCP-47 locale tag).");
11750
+ }
11751
+ return lang;
11752
+ }
11753
+ function readSection(args) {
11754
+ const section = args.section;
11755
+ if (typeof section !== "string" || !isProfileSection(section)) {
11756
+ throw new McpRequestError(`\`section\` must be one of: ${MCP_PROFILE_SECTIONS.join(", ")}.`);
11757
+ }
11758
+ return section;
11759
+ }
11760
+ function isProfileSection(value) {
11761
+ return MCP_PROFILE_SECTIONS.includes(value);
11762
+ }
11763
+
11590
11764
  // src/image.ts
11591
11765
  var ACCEPTED_IMAGE_MIME_TYPES = [
11592
11766
  "image/jpeg",
@@ -11922,11 +12096,17 @@ var SCHEMA_VERSION = "0.5.0";
11922
12096
  export {
11923
12097
  ACCEPTED_IMAGE_MIME_TYPES,
11924
12098
  ConflictError,
12099
+ DARK_PALETTE,
11925
12100
  IMAGE_EXTENSIONS,
11926
12101
  ImportError,
12102
+ LIGHT_PALETTE,
11927
12103
  MAX_IMAGE_BYTES,
11928
12104
  MAX_IMAGE_DIMENSION,
11929
12105
  MAX_IMAGE_FRAMES,
12106
+ MCP_PROFILE_SECTIONS,
12107
+ MCP_RESOURCES,
12108
+ MCP_TOOLS,
12109
+ McpRequestError,
11930
12110
  MigrationError,
11931
12111
  NotFoundError,
11932
12112
  RANK_FULL_CODING_HOURS,
@@ -11940,6 +12120,7 @@ export {
11940
12120
  deriveCv,
11941
12121
  deriveRankTier,
11942
12122
  detectImageMime,
12123
+ executeMcpTool,
11943
12124
  exportTakuhon,
11944
12125
  formatCodingTime,
11945
12126
  generateJsonLd,
@@ -11951,6 +12132,7 @@ export {
11951
12132
  migrations,
11952
12133
  normalize,
11953
12134
  readImageInfo,
12135
+ readMcpResource,
11954
12136
  renderActivitySvg,
11955
12137
  resolveLocale,
11956
12138
  schema,