@utilix-tech/mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.js +190 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 102 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
|
|
4
4
|
|
|
5
5
|
No API key required. All tools run locally. Requires Node.js 18 or later.
|
|
6
6
|
|
|
@@ -230,10 +230,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
230
230
|
| `compress_json` | Compress JSON for LLM context budgets |
|
|
231
231
|
| `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
|
|
232
232
|
|
|
233
|
-
### Image (
|
|
233
|
+
### Image (2 tools)
|
|
234
234
|
| Tool | Description |
|
|
235
235
|
|------|-------------|
|
|
236
236
|
| `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
|
|
237
|
+
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
237
238
|
|
|
238
239
|
### PDF (1 tool)
|
|
239
240
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -1641,6 +1641,196 @@ server.tool("read_pdf_metadata", "Read PDF metadata (title, author, dates, page
|
|
|
1641
1641
|
if ("error" in result) return errText(result);
|
|
1642
1642
|
return text(JSON.stringify(result, null, 2));
|
|
1643
1643
|
});
|
|
1644
|
+
var _EXIF_ORIENTATION_LABELS = {
|
|
1645
|
+
1: "Normal",
|
|
1646
|
+
2: "Mirrored horizontal",
|
|
1647
|
+
3: "Rotated 180\xB0",
|
|
1648
|
+
4: "Mirrored vertical",
|
|
1649
|
+
5: "Mirrored horizontal, rotated 90\xB0 CCW",
|
|
1650
|
+
6: "Rotated 90\xB0 CW",
|
|
1651
|
+
7: "Mirrored horizontal, rotated 90\xB0 CW",
|
|
1652
|
+
8: "Rotated 90\xB0 CCW"
|
|
1653
|
+
};
|
|
1654
|
+
function _tu16(r, o) {
|
|
1655
|
+
return r.view.getUint16(o, r.little);
|
|
1656
|
+
}
|
|
1657
|
+
function _tu32(r, o) {
|
|
1658
|
+
return r.view.getUint32(o, r.little);
|
|
1659
|
+
}
|
|
1660
|
+
function _readIfdEntries(r, ifdOffset) {
|
|
1661
|
+
if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
|
|
1662
|
+
const numEntries = _tu16(r, ifdOffset);
|
|
1663
|
+
const entries = [];
|
|
1664
|
+
for (let i = 0; i < numEntries; i++) {
|
|
1665
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
1666
|
+
if (entryOffset + 12 > r.bytes.length) break;
|
|
1667
|
+
entries.push({ tag: _tu16(r, entryOffset), type: _tu16(r, entryOffset + 2), count: _tu32(r, entryOffset + 4), valueOffsetField: entryOffset + 8 });
|
|
1668
|
+
}
|
|
1669
|
+
return { entries };
|
|
1670
|
+
}
|
|
1671
|
+
function _findExifEntry(entries, tag) {
|
|
1672
|
+
return entries.find((e) => e.tag === tag);
|
|
1673
|
+
}
|
|
1674
|
+
function _readExifAscii(r, entry) {
|
|
1675
|
+
const size = entry.count;
|
|
1676
|
+
const inline = size <= 4;
|
|
1677
|
+
const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + _tu32(r, entry.valueOffsetField);
|
|
1678
|
+
if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
|
|
1679
|
+
let out = "";
|
|
1680
|
+
for (let i = 0; i < size; i++) {
|
|
1681
|
+
const c = r.bytes[dataOffset + i];
|
|
1682
|
+
if (c === 0) break;
|
|
1683
|
+
out += String.fromCharCode(c);
|
|
1684
|
+
}
|
|
1685
|
+
const trimmed = out.trim();
|
|
1686
|
+
return trimmed || null;
|
|
1687
|
+
}
|
|
1688
|
+
function _readExifRational(r, entry) {
|
|
1689
|
+
const dataOffset = r.tiffStart + _tu32(r, entry.valueOffsetField);
|
|
1690
|
+
if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
|
|
1691
|
+
return { num: _tu32(r, dataOffset), den: _tu32(r, dataOffset + 4) };
|
|
1692
|
+
}
|
|
1693
|
+
function _readExifGpsCoordinate(r, entries, valueTag, refTag) {
|
|
1694
|
+
const valueEntry = _findExifEntry(entries, valueTag);
|
|
1695
|
+
if (!valueEntry) return null;
|
|
1696
|
+
const dataOffset = r.tiffStart + _tu32(r, valueEntry.valueOffsetField);
|
|
1697
|
+
if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
|
|
1698
|
+
const deg = _tu32(r, dataOffset) / (_tu32(r, dataOffset + 4) || 1);
|
|
1699
|
+
const min = _tu32(r, dataOffset + 8) / (_tu32(r, dataOffset + 12) || 1);
|
|
1700
|
+
const sec = _tu32(r, dataOffset + 16) / (_tu32(r, dataOffset + 20) || 1);
|
|
1701
|
+
let decimal = deg + min / 60 + sec / 3600;
|
|
1702
|
+
const refEntry = _findExifEntry(entries, refTag);
|
|
1703
|
+
const ref = refEntry ? _readExifAscii(r, refEntry) : null;
|
|
1704
|
+
if (ref === "S" || ref === "W") decimal = -decimal;
|
|
1705
|
+
return Math.round(decimal * 1e6) / 1e6;
|
|
1706
|
+
}
|
|
1707
|
+
function _gcdExif(a, b) {
|
|
1708
|
+
return b === 0 ? a : _gcdExif(b, a % b);
|
|
1709
|
+
}
|
|
1710
|
+
function _round2Exif(n) {
|
|
1711
|
+
return Math.round(n * 100) / 100;
|
|
1712
|
+
}
|
|
1713
|
+
function _formatExposureTime(num, den) {
|
|
1714
|
+
if (num === 0) return "0s";
|
|
1715
|
+
if (num >= den) return `${_round2Exif(num / den)}s`;
|
|
1716
|
+
const divisor = _gcdExif(num, den) || 1;
|
|
1717
|
+
const n = num / divisor;
|
|
1718
|
+
const d = den / divisor;
|
|
1719
|
+
return `1/${Math.round(d / n)}`;
|
|
1720
|
+
}
|
|
1721
|
+
function _readExifData(bytes) {
|
|
1722
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
1723
|
+
return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
|
|
1724
|
+
}
|
|
1725
|
+
let offset = 2;
|
|
1726
|
+
let exifStart = -1;
|
|
1727
|
+
while (offset + 4 <= bytes.length) {
|
|
1728
|
+
if (bytes[offset] !== 255) {
|
|
1729
|
+
offset++;
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const marker = bytes[offset + 1];
|
|
1733
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
1734
|
+
offset += 2;
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (marker === 217 || marker === 218) break;
|
|
1738
|
+
const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
1739
|
+
if (marker === 225) {
|
|
1740
|
+
const idOffset = offset + 4;
|
|
1741
|
+
if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
|
|
1742
|
+
exifStart = idOffset + 6;
|
|
1743
|
+
break;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
offset += 2 + segmentLength;
|
|
1747
|
+
}
|
|
1748
|
+
if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
|
|
1749
|
+
if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
|
|
1750
|
+
const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
|
|
1751
|
+
if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
|
|
1752
|
+
const little = byteOrder === "II";
|
|
1753
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1754
|
+
const r = { bytes, view, little, tiffStart: exifStart };
|
|
1755
|
+
if (_tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
|
|
1756
|
+
const ifd0Offset = exifStart + _tu32(r, exifStart + 4);
|
|
1757
|
+
const ifd0 = _readIfdEntries(r, ifd0Offset);
|
|
1758
|
+
if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
|
|
1759
|
+
const result = { tagCount: ifd0.entries.length };
|
|
1760
|
+
const makeEntry = _findExifEntry(ifd0.entries, 271);
|
|
1761
|
+
if (makeEntry) {
|
|
1762
|
+
const v = _readExifAscii(r, makeEntry);
|
|
1763
|
+
if (v) result.make = v;
|
|
1764
|
+
}
|
|
1765
|
+
const modelEntry = _findExifEntry(ifd0.entries, 272);
|
|
1766
|
+
if (modelEntry) {
|
|
1767
|
+
const v = _readExifAscii(r, modelEntry);
|
|
1768
|
+
if (v) result.model = v;
|
|
1769
|
+
}
|
|
1770
|
+
const softwareEntry = _findExifEntry(ifd0.entries, 305);
|
|
1771
|
+
if (softwareEntry) {
|
|
1772
|
+
const v = _readExifAscii(r, softwareEntry);
|
|
1773
|
+
if (v) result.software = v;
|
|
1774
|
+
}
|
|
1775
|
+
const dateTimeEntry = _findExifEntry(ifd0.entries, 306);
|
|
1776
|
+
if (dateTimeEntry) {
|
|
1777
|
+
const v = _readExifAscii(r, dateTimeEntry);
|
|
1778
|
+
if (v) result.dateTime = v;
|
|
1779
|
+
}
|
|
1780
|
+
const orientEntry = _findExifEntry(ifd0.entries, 274);
|
|
1781
|
+
if (orientEntry) {
|
|
1782
|
+
const v = _tu16(r, orientEntry.valueOffsetField);
|
|
1783
|
+
result.orientation = v;
|
|
1784
|
+
result.orientationLabel = _EXIF_ORIENTATION_LABELS[v] ?? "Unknown";
|
|
1785
|
+
}
|
|
1786
|
+
const exifIfdPtr = _findExifEntry(ifd0.entries, 34665);
|
|
1787
|
+
if (exifIfdPtr) {
|
|
1788
|
+
const subOffset = exifStart + _tu32(r, exifIfdPtr.valueOffsetField);
|
|
1789
|
+
const sub = _readIfdEntries(r, subOffset);
|
|
1790
|
+
if (sub) {
|
|
1791
|
+
const dtoEntry = _findExifEntry(sub.entries, 36867);
|
|
1792
|
+
if (dtoEntry) {
|
|
1793
|
+
const v = _readExifAscii(r, dtoEntry);
|
|
1794
|
+
if (v) result.dateTimeOriginal = v;
|
|
1795
|
+
}
|
|
1796
|
+
const expEntry = _findExifEntry(sub.entries, 33434);
|
|
1797
|
+
if (expEntry) {
|
|
1798
|
+
const rat = _readExifRational(r, expEntry);
|
|
1799
|
+
if (rat && rat.den !== 0) result.exposureTime = _formatExposureTime(rat.num, rat.den);
|
|
1800
|
+
}
|
|
1801
|
+
const fEntry = _findExifEntry(sub.entries, 33437);
|
|
1802
|
+
if (fEntry) {
|
|
1803
|
+
const rat = _readExifRational(r, fEntry);
|
|
1804
|
+
if (rat && rat.den !== 0) result.fNumber = _round2Exif(rat.num / rat.den);
|
|
1805
|
+
}
|
|
1806
|
+
const isoEntry = _findExifEntry(sub.entries, 34855);
|
|
1807
|
+
if (isoEntry) result.isoSpeed = _tu16(r, isoEntry.valueOffsetField);
|
|
1808
|
+
const focalEntry = _findExifEntry(sub.entries, 37386);
|
|
1809
|
+
if (focalEntry) {
|
|
1810
|
+
const rat = _readExifRational(r, focalEntry);
|
|
1811
|
+
if (rat && rat.den !== 0) result.focalLength = _round2Exif(rat.num / rat.den);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
const gpsIfdPtr = _findExifEntry(ifd0.entries, 34853);
|
|
1816
|
+
if (gpsIfdPtr) {
|
|
1817
|
+
const gpsOffset = exifStart + _tu32(r, gpsIfdPtr.valueOffsetField);
|
|
1818
|
+
const gps = _readIfdEntries(r, gpsOffset);
|
|
1819
|
+
if (gps) {
|
|
1820
|
+
const lat = _readExifGpsCoordinate(r, gps.entries, 2, 1);
|
|
1821
|
+
const lon = _readExifGpsCoordinate(r, gps.entries, 4, 3);
|
|
1822
|
+
if (lat !== null) result.gpsLatitude = lat;
|
|
1823
|
+
if (lon !== null) result.gpsLongitude = lon;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
server.tool("read_exif_data", "Read common EXIF tags (camera make/model, orientation, timestamps, exposure settings, GPS coordinates) directly from a JPEG's APP1 segment. Only JPEG is supported.", { image_base64: z.string().describe("Base64-encoded JPEG bytes, optionally prefixed with a data: URL scheme") }, async ({ image_base64 }) => {
|
|
1829
|
+
const bytes = new Uint8Array(Buffer.from(image_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
1830
|
+
const result = _readExifData(bytes);
|
|
1831
|
+
if ("error" in result) return errText(result);
|
|
1832
|
+
return text(JSON.stringify(result, null, 2));
|
|
1833
|
+
});
|
|
1644
1834
|
function _parseJwks(input) {
|
|
1645
1835
|
const trimmed = input.trim();
|
|
1646
1836
|
if (!trimmed) return { error: "No input provided" };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server exposing 102 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
|
|
5
5
|
"author": "Utilix <hello@utilix.tech>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://utilix.tech/mcp",
|